diff --git a/.coveragerc b/.coveragerc index 50fcf151821..0cadadfc5e8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -221,6 +221,7 @@ omit = homeassistant/components/ecobee/weather.py homeassistant/components/econet/__init__.py homeassistant/components/econet/binary_sensor.py + homeassistant/components/econet/climate.py homeassistant/components/econet/const.py homeassistant/components/econet/sensor.py homeassistant/components/econet/water_heater.py @@ -314,7 +315,6 @@ omit = homeassistant/components/foscam/camera.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py - homeassistant/components/freebox/__init__.py homeassistant/components/freebox/device_tracker.py homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py @@ -377,6 +377,9 @@ omit = homeassistant/components/harmony/data.py homeassistant/components/harmony/remote.py homeassistant/components/harmony/util.py + homeassistant/components/hassio/binary_sensor.py + homeassistant/components/hassio/entity.py + homeassistant/components/hassio/sensor.py homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py @@ -384,7 +387,13 @@ omit = homeassistant/components/hikvisioncam/switch.py homeassistant/components/hisense_aehw4a1/* homeassistant/components/hitron_coda/device_tracker.py - homeassistant/components/hive/* + homeassistant/components/hive/__init__.py + homeassistant/components/hive/climate.py + homeassistant/components/hive/binary_sensor.py + homeassistant/components/hive/light.py + homeassistant/components/hive/sensor.py + homeassistant/components/hive/switch.py + homeassistant/components/hive/water_heater.py homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/switch.py homeassistant/components/home_connect/* @@ -392,6 +401,9 @@ omit = homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py homeassistant/components/homematic/notify.py + homeassistant/components/home_plus_control/api.py + homeassistant/components/home_plus_control/helpers.py + homeassistant/components/home_plus_control/switch.py homeassistant/components/homeworks/* homeassistant/components/honeywell/climate.py homeassistant/components/horizon/media_player.py @@ -497,7 +509,18 @@ omit = homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/const.py homeassistant/components/launch_library/sensor.py - homeassistant/components/lcn/* + homeassistant/components/lcn/__init__.py + homeassistant/components/lcn/binary_sensor.py + homeassistant/components/lcn/climate.py + homeassistant/components/lcn/const.py + homeassistant/components/lcn/cover.py + homeassistant/components/lcn/helpers.py + homeassistant/components/lcn/light.py + homeassistant/components/lcn/scene.py + homeassistant/components/lcn/schemas.py + homeassistant/components/lcn/sensor.py + homeassistant/components/lcn/services.py + homeassistant/components/lcn/switch.py homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_soundbar/media_player.py homeassistant/components/life360/* @@ -540,7 +563,6 @@ omit = homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/* - homeassistant/components/maxcube/* homeassistant/components/mcp23017/* homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py @@ -624,17 +646,6 @@ omit = homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/legacy/* - homeassistant/components/netatmo/__init__.py - homeassistant/components/netatmo/api.py - homeassistant/components/netatmo/camera.py - homeassistant/components/netatmo/climate.py - homeassistant/components/netatmo/const.py - homeassistant/components/netatmo/data_handler.py - homeassistant/components/netatmo/helper.py - homeassistant/components/netatmo/light.py - homeassistant/components/netatmo/netatmo_entity_base.py - homeassistant/components/netatmo/sensor.py - homeassistant/components/netatmo/webhook.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear_lte/* @@ -723,12 +734,14 @@ omit = homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/media_player.py + homeassistant/components/philips_js/remote.py homeassistant/components/pi_hole/sensor.py homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py homeassistant/components/pi4ioe5v9xxxx/switch.py homeassistant/components/picotts/tts.py homeassistant/components/piglow/light.py homeassistant/components/pilight/* + homeassistant/components/ping/__init__.py homeassistant/components/ping/const.py homeassistant/components/ping/binary_sensor.py homeassistant/components/ping/device_tracker.py @@ -782,6 +795,7 @@ omit = homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py + homeassistant/components/recorder/repack.py homeassistant/components/recswitch/switch.py homeassistant/components/reddit/* homeassistant/components/rejseplanen/sensor.py @@ -823,6 +837,11 @@ omit = homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py + homeassistant/components/screenlogic/__init__.py + homeassistant/components/screenlogic/binary_sensor.py + homeassistant/components/screenlogic/climate.py + homeassistant/components/screenlogic/sensor.py + homeassistant/components/screenlogic/switch.py homeassistant/components/scsgate/* homeassistant/components/scsgate/cover.py homeassistant/components/sendgrid/notify.py @@ -1045,7 +1064,14 @@ omit = homeassistant/components/velbus/switch.py homeassistant/components/velux/* homeassistant/components/venstar/climate.py - homeassistant/components/verisure/* + homeassistant/components/verisure/__init__.py + homeassistant/components/verisure/alarm_control_panel.py + homeassistant/components/verisure/binary_sensor.py + homeassistant/components/verisure/camera.py + homeassistant/components/verisure/coordinator.py + homeassistant/components/verisure/lock.py + homeassistant/components/verisure/sensor.py + homeassistant/components/verisure/switch.py homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py @@ -1164,3 +1190,6 @@ exclude_lines = # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError + + # TYPE_CHECKING block is never executed during pytest run + if TYPE_CHECKING: diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..ad3205c51c8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: https://www.nabucasa.com +github: balloob diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9a46dd82215..aa81d6e4df7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Report an issue with Home Assistant Core -about: Report an issue with Home Assistant Core. +description: Report an issue with Home Assistant Core. title: "" issue_body: true body: @@ -26,6 +26,7 @@ body: value: | ## Environment - type: input + id: version validations: required: true attributes: @@ -52,11 +53,13 @@ body: - Home Assistant Supervised - Home Assistant Core - type: input + id: integration_name attributes: label: Integration causing the issue description: > The name of the integration, for example, Automation or Philips Hue. - type: input + id: integration_link attributes: label: Link to integration documentation on our website placeholder: "https://www.home-assistant.io/integrations/..." @@ -76,20 +79,12 @@ body: description: | If this issue has an example piece of YAML that can help reproducing this problem, please provide. This can be an piece of YAML from, e.g., an automation, script, scene or configuration. - value: | - ```yaml - # Put your YAML below this line - - ``` + render: yaml - type: textarea attributes: label: Anything in the logs that might be useful for us? description: For example, error message, or stack traces. - value: | - ```txt - # Put your logs below this line - - ``` + render: txt - type: markdown attributes: value: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 28410123914..afee814b432 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ on: env: CACHE_VERSION: 1 DEFAULT_PYTHON: 3.8 - PRE_COMMIT_HOME: ~/.cache/pre-commit + PRE_COMMIT_CACHE: ~/.cache/pre-commit jobs: # Separate job to pre-populate the base dependency cache @@ -20,6 +20,9 @@ jobs: prepare-base: name: Prepare base dependencies runs-on: ubuntu-latest + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 @@ -28,21 +31,25 @@ jobs: uses: actions/setup-python@v2.2.1 with: python-version: ${{ env.DEFAULT_PYTHON }} + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v2.1.4 with: path: venv key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -50,15 +57,20 @@ jobs: . venv/bin/activate pip install -U "pip<20.3" setuptools pip install -r requirements.txt -r requirements_test.txt + - name: Generate partial pre-commit restore key + id: generate-pre-commit-key + run: >- + echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- + ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -82,12 +94,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -97,13 +105,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run bandit run: | @@ -127,12 +134,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -142,13 +145,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run black run: | @@ -172,12 +174,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -187,13 +185,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Register codespell problem matcher run: | @@ -239,12 +236,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -254,13 +247,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Register check executables problem matcher run: | @@ -287,12 +279,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -302,13 +290,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Register flake8 problem matcher run: | @@ -335,12 +322,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -350,13 +333,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run isort run: | @@ -380,12 +362,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -395,13 +373,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Register check-json problem matcher run: | @@ -428,12 +405,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -443,13 +416,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run pyupgrade run: | @@ -484,12 +456,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -499,13 +467,12 @@ jobs: id: cache-precommit uses: actions/cache@v2.1.4 with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' + path: ${{ env.PRE_COMMIT_CACHE }} + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' run: | - echo "Failed to restore Python virtual environment from cache" + echo "Failed to restore pre-commit environment from cache" exit 1 - name: Register yamllint problem matcher run: | @@ -531,11 +498,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -563,12 +527,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -585,24 +545,31 @@ jobs: strategy: matrix: python-version: [3.8, 3.9] + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }}" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.4 with: path: venv key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + ${{ runner.os }}-${{ matrix.python-version }}-${{ + steps.generate-python-key.outputs.key }} restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- + ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- + ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- + ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -633,11 +600,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -667,11 +631,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -689,6 +650,7 @@ jobs: runs-on: ubuntu-latest needs: prepare-tests strategy: + fail-fast: false matrix: group: [1, 2, 3, 4] python-version: [3.8, 3.9] @@ -703,11 +665,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -735,6 +694,7 @@ jobs: --test-group-count 4 \ --test-group=${{ matrix.group }} \ --cov homeassistant \ + --cov-report= \ -o console_output_style=count \ -p no:sugar \ tests @@ -750,7 +710,7 @@ jobs: coverage: name: Process test coverage runs-on: ubuntu-latest - needs: pytest + needs: ["prepare-tests", "pytest"] strategy: matrix: python-version: [3.8] @@ -763,11 +723,8 @@ jobs: uses: actions/cache@v2.1.4 with: path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }} + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + needs.prepare-tests.outputs.python-key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -782,4 +739,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.2.1 + uses: codecov/codecov-action@v1.3.1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a280d0ee89a..3ff0f47cedc 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v3.0.17 + uses: actions/stale@v3.0.18 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -53,7 +53,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v3.0.17 + uses: actions/stale@v3.0.18 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -78,7 +78,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v3.0.17 + uses: actions/stale@v3.0.18 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b650ebac79..2f4aea74ae9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.7.2 + rev: v2.11.0 hooks: - id: pyupgrade args: [--py38-plus] @@ -23,12 +23,16 @@ repos: exclude_types: [csv, json] exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + rev: 3.9.0 hooks: - id: flake8 additional_dependencies: - - flake8-docstrings==1.5.0 - - pydocstyle==5.1.1 + - pycodestyle==2.7.0 + - pyflakes==2.3.1 + - flake8-docstrings==1.6.0 + - pydocstyle==6.0.0 + - flake8-comprehensions==3.4.0 + - flake8-noqa==1.1.0 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.7.0 @@ -40,7 +44,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.5.3 + rev: 5.7.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks @@ -64,6 +68,19 @@ repos: hooks: - id: prettier stages: [manual] + - repo: https://github.com/cdce8p/python-typing-update + rev: v0.3.2 + hooks: + # Run `python-typing-update` hook manually from time to time + # to update python typing syntax. + # Will require manual work, before submitting changes! + - id: python-typing-update + stages: [manual] + args: + - --py38-plus + - --force + - --keep-updates + files: ^(homeassistant|tests|script)/.+\.py$ - repo: local hooks: # Run mypy through our wrapper script in order to get the possible diff --git a/CODEOWNERS b/CODEOWNERS index 0e069f94e73..70ea2385da8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -36,6 +36,7 @@ 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 homeassistant/components/api/* @home-assistant/core @@ -180,14 +181,12 @@ homeassistant/components/google_cloud/* @lufton homeassistant/components/gpsd/* @fabaff homeassistant/components/gree/* @cmroche homeassistant/components/greeneye_monitor/* @jkeljo -homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/guardian/* @bachya homeassistant/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey homeassistant/components/hassio/* @home-assistant/supervisor -homeassistant/components/hdmi_cec/* @newAM homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger @@ -198,11 +197,11 @@ homeassistant/components/history/* @home-assistant/core homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/hlk_sw16/* @jameshilliard homeassistant/components/home_connect/* @DavidMStraub +homeassistant/components/home_plus_control/* @chemaaa homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homematic/* @pvizeli @danielperna84 -homeassistant/components/homematicip_cloud/* @SukramJ homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis @@ -403,6 +402,7 @@ homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff +homeassistant/components/screenlogic/* @dieselrabbit homeassistant/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core homeassistant/components/sense/* @kbickar @@ -433,12 +433,12 @@ homeassistant/components/smarttub/* @mdz homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/sms/* @ocalvo homeassistant/components/smtp/* @fabaff +homeassistant/components/solaredge/* @frenck homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne -homeassistant/components/somfy_mylink/* @bdraco homeassistant/components/sonarr/* @ctalkington homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/sonos/* @cgtobi @@ -454,7 +454,7 @@ homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stookalert/* @fwestenberg -homeassistant/components/stream/* @hunterjm @uvjustin +homeassistant/components/stream/* @hunterjm @uvjustin @allenporter homeassistant/components/stt/* @pvizeli homeassistant/components/subaru/* @G-Two homeassistant/components/suez_water/* @ooii @@ -492,6 +492,7 @@ homeassistant/components/toon/* @frenck homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus +homeassistant/components/trace/* @home-assistant/core homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins @@ -512,7 +513,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 -homeassistant/components/vera/* @vangorra +homeassistant/components/vera/* @pavoni homeassistant/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff @ludeeus @@ -524,6 +525,7 @@ homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf @dmcc homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund +homeassistant/components/wake_on_lan/* @ntilley905 homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff diff --git a/Dockerfile.dev b/Dockerfile.dev index 09f8f155930..68188f16f01 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -28,10 +28,11 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements -COPY requirements_test.txt requirements_test_pre_commit.txt ./ +COPY requirements.txt requirements_test.txt requirements_test_pre_commit.txt ./ COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt -RUN pip3 install -r requirements_test.txt \ - && rm -rf requirements_test.txt requirements_test_pre_commit.txt homeassistant/ +RUN pip3 install -r requirements.txt \ + && pip3 install -r requirements_test.txt \ + && rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/ # Set the default shell to bash instead of sh ENV SHELL /bin/bash diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 840e0bed24d..d8256e2ef92 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,11 +1,12 @@ """Start Home Assistant.""" +from __future__ import annotations + import argparse import os import platform import subprocess import sys import threading -from typing import List from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ @@ -206,7 +207,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None: pass -def cmdline() -> List[str]: +def cmdline() -> list[str]: """Collect path and arguments to re-execute the current hass instance.""" if os.path.basename(sys.argv[0]) == "__main__.py": modulepath = os.path.dirname(sys.argv[0]) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 7d6f94dda85..3830419c537 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict from datetime import timedelta -from typing import Any, Dict, List, Optional, Tuple, cast +from typing import Any, Dict, Optional, Tuple, cast import jwt @@ -36,8 +36,8 @@ class InvalidProvider(Exception): async def auth_manager_from_config( hass: HomeAssistant, - provider_configs: List[Dict[str, Any]], - module_configs: List[Dict[str, Any]], + provider_configs: list[dict[str, Any]], + module_configs: list[dict[str, Any]], ) -> AuthManager: """Initialize an auth manager from config. @@ -87,8 +87,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): self, handler_key: Any, *, - context: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> data_entry_flow.FlowHandler: """Create a login flow.""" auth_provider = self.auth_manager.get_auth_provider(*handler_key) @@ -97,8 +97,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: dict[str, Any] + ) -> dict[str, Any]: """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) @@ -157,22 +157,22 @@ class AuthManager: self.login_flow = AuthManagerFlowManager(hass, self) @property - def auth_providers(self) -> List[AuthProvider]: + def auth_providers(self) -> list[AuthProvider]: """Return a list of available auth providers.""" return list(self._providers.values()) @property - def auth_mfa_modules(self) -> List[MultiFactorAuthModule]: + def auth_mfa_modules(self) -> list[MultiFactorAuthModule]: """Return a list of available auth modules.""" return list(self._mfa_modules.values()) def get_auth_provider( - self, provider_type: str, provider_id: Optional[str] - ) -> Optional[AuthProvider]: + self, provider_type: str, provider_id: str | None + ) -> AuthProvider | None: """Return an auth provider, None if not found.""" return self._providers.get((provider_type, provider_id)) - def get_auth_providers(self, provider_type: str) -> List[AuthProvider]: + def get_auth_providers(self, provider_type: str) -> list[AuthProvider]: """Return a List of auth provider of one type, Empty if not found.""" return [ provider @@ -180,30 +180,30 @@ class AuthManager: if p_type == provider_type ] - def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]: + def get_auth_mfa_module(self, module_id: str) -> MultiFactorAuthModule | None: """Return a multi-factor auth module, None if not found.""" return self._mfa_modules.get(module_id) - async def async_get_users(self) -> List[models.User]: + async def async_get_users(self) -> list[models.User]: """Retrieve all users.""" return await self._store.async_get_users() - async def async_get_user(self, user_id: str) -> Optional[models.User]: + async def async_get_user(self, user_id: str) -> models.User | None: """Retrieve a user.""" return await self._store.async_get_user(user_id) - async def async_get_owner(self) -> Optional[models.User]: + async def async_get_owner(self) -> models.User | None: """Retrieve the owner.""" users = await self.async_get_users() return next((user for user in users if user.is_owner), None) - async def async_get_group(self, group_id: str) -> Optional[models.Group]: + async def async_get_group(self, group_id: str) -> models.Group | None: """Retrieve all groups.""" return await self._store.async_get_group(group_id) async def async_get_user_by_credentials( self, credentials: models.Credentials - ) -> Optional[models.User]: + ) -> models.User | None: """Get a user by credential, return None if not found.""" for user in await self.async_get_users(): for creds in user.credentials: @@ -213,7 +213,7 @@ class AuthManager: return None async def async_create_system_user( - self, name: str, group_ids: Optional[List[str]] = None + self, name: str, group_ids: list[str] | None = None ) -> models.User: """Create a system user.""" user = await self._store.async_create_user( @@ -225,10 +225,10 @@ class AuthManager: return user async def async_create_user( - self, name: str, group_ids: Optional[List[str]] = None + self, name: str, group_ids: list[str] | None = None ) -> models.User: """Create a user.""" - kwargs: Dict[str, Any] = { + kwargs: dict[str, Any] = { "name": name, "is_active": True, "group_ids": group_ids or [], @@ -294,12 +294,12 @@ class AuthManager: async def async_update_user( self, user: models.User, - name: Optional[str] = None, - is_active: Optional[bool] = None, - group_ids: Optional[List[str]] = None, + name: str | None = None, + is_active: bool | None = None, + group_ids: list[str] | None = None, ) -> None: """Update a user.""" - kwargs: Dict[str, Any] = {} + kwargs: dict[str, Any] = {} if name is not None: kwargs["name"] = name if group_ids is not None: @@ -362,9 +362,9 @@ class AuthManager: await module.async_depose_user(user.id) - async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]: + async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]: """List enabled mfa modules for user.""" - modules: Dict[str, str] = OrderedDict() + modules: dict[str, str] = OrderedDict() for module_id, module in self._mfa_modules.items(): if await module.async_is_user_setup(user.id): modules[module_id] = module.name @@ -373,12 +373,12 @@ class AuthManager: async def async_create_refresh_token( self, user: models.User, - client_id: Optional[str] = None, - client_name: Optional[str] = None, - client_icon: Optional[str] = None, - token_type: Optional[str] = None, + client_id: str | None = None, + client_name: str | None = None, + client_icon: str | None = None, + token_type: str | None = None, access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, - credential: Optional[models.Credentials] = None, + credential: models.Credentials | None = None, ) -> models.RefreshToken: """Create a new refresh token for a user.""" if not user.is_active: @@ -432,13 +432,13 @@ class AuthManager: async def async_get_refresh_token( self, token_id: str - ) -> Optional[models.RefreshToken]: + ) -> models.RefreshToken | None: """Get refresh token by id.""" return await self._store.async_get_refresh_token(token_id) async def async_get_refresh_token_by_token( self, token: str - ) -> Optional[models.RefreshToken]: + ) -> models.RefreshToken | None: """Get refresh token by token.""" return await self._store.async_get_refresh_token_by_token(token) @@ -450,7 +450,7 @@ class AuthManager: @callback def async_create_access_token( - self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None + self, refresh_token: models.RefreshToken, remote_ip: str | None = None ) -> str: """Create a new access token.""" self.async_validate_refresh_token(refresh_token, remote_ip) @@ -471,7 +471,7 @@ class AuthManager: @callback def _async_resolve_provider( self, refresh_token: models.RefreshToken - ) -> Optional[AuthProvider]: + ) -> AuthProvider | None: """Get the auth provider for the given refresh token. Raises an exception if the expected provider is no longer available or return @@ -492,7 +492,7 @@ class AuthManager: @callback def async_validate_refresh_token( - self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None + self, refresh_token: models.RefreshToken, remote_ip: str | None = None ) -> None: """Validate that a refresh token is usable. @@ -504,7 +504,7 @@ class AuthManager: async def async_validate_access_token( self, token: str - ) -> Optional[models.RefreshToken]: + ) -> models.RefreshToken | None: """Return refresh token if an access token is valid.""" try: unverif_claims = jwt.decode(token, verify=False) @@ -535,7 +535,7 @@ class AuthManager: @callback def _async_get_auth_provider( self, credentials: models.Credentials - ) -> Optional[AuthProvider]: + ) -> AuthProvider | None: """Get auth provider from a set of credentials.""" auth_provider_key = ( credentials.auth_provider_type, diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 724f1c86722..0b360668ad4 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -1,10 +1,12 @@ """Storage for auth models.""" +from __future__ import annotations + import asyncio from collections import OrderedDict from datetime import timedelta import hmac from logging import getLogger -from typing import Any, Dict, List, Optional +from typing import Any from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback @@ -34,15 +36,15 @@ class AuthStore: def __init__(self, hass: HomeAssistant) -> None: """Initialize the auth store.""" self.hass = hass - self._users: Optional[Dict[str, models.User]] = None - self._groups: Optional[Dict[str, models.Group]] = None - self._perm_lookup: Optional[PermissionLookup] = None + self._users: dict[str, models.User] | None = None + self._groups: dict[str, models.Group] | None = None + self._perm_lookup: PermissionLookup | None = None self._store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) self._lock = asyncio.Lock() - async def async_get_groups(self) -> List[models.Group]: + async def async_get_groups(self) -> list[models.Group]: """Retrieve all users.""" if self._groups is None: await self._async_load() @@ -50,7 +52,7 @@ class AuthStore: return list(self._groups.values()) - async def async_get_group(self, group_id: str) -> Optional[models.Group]: + async def async_get_group(self, group_id: str) -> models.Group | None: """Retrieve all users.""" if self._groups is None: await self._async_load() @@ -58,7 +60,7 @@ class AuthStore: return self._groups.get(group_id) - async def async_get_users(self) -> List[models.User]: + async def async_get_users(self) -> list[models.User]: """Retrieve all users.""" if self._users is None: await self._async_load() @@ -66,7 +68,7 @@ class AuthStore: return list(self._users.values()) - async def async_get_user(self, user_id: str) -> Optional[models.User]: + async def async_get_user(self, user_id: str) -> models.User | None: """Retrieve a user by id.""" if self._users is None: await self._async_load() @@ -76,12 +78,12 @@ class AuthStore: async def async_create_user( self, - name: Optional[str], - is_owner: Optional[bool] = None, - is_active: Optional[bool] = None, - system_generated: Optional[bool] = None, - credentials: Optional[models.Credentials] = None, - group_ids: Optional[List[str]] = None, + name: str | None, + is_owner: bool | None = None, + is_active: bool | None = None, + system_generated: bool | None = None, + credentials: models.Credentials | None = None, + group_ids: list[str] | None = None, ) -> models.User: """Create a new user.""" if self._users is None: @@ -97,7 +99,7 @@ class AuthStore: raise ValueError(f"Invalid group specified {group_id}") groups.append(group) - kwargs: Dict[str, Any] = { + kwargs: dict[str, Any] = { "name": name, # Until we get group management, we just put everyone in the # same group. @@ -146,9 +148,9 @@ class AuthStore: async def async_update_user( self, user: models.User, - name: Optional[str] = None, - is_active: Optional[bool] = None, - group_ids: Optional[List[str]] = None, + name: str | None = None, + is_active: bool | None = None, + group_ids: list[str] | None = None, ) -> None: """Update a user.""" assert self._groups is not None @@ -203,15 +205,15 @@ class AuthStore: async def async_create_refresh_token( self, user: models.User, - client_id: Optional[str] = None, - client_name: Optional[str] = None, - client_icon: Optional[str] = None, + client_id: str | None = None, + client_name: str | None = None, + client_icon: str | None = None, token_type: str = models.TOKEN_TYPE_NORMAL, access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, - credential: Optional[models.Credentials] = None, + credential: models.Credentials | None = None, ) -> models.RefreshToken: """Create a new token for a user.""" - kwargs: Dict[str, Any] = { + kwargs: dict[str, Any] = { "user": user, "client_id": client_id, "token_type": token_type, @@ -244,7 +246,7 @@ class AuthStore: async def async_get_refresh_token( self, token_id: str - ) -> Optional[models.RefreshToken]: + ) -> models.RefreshToken | None: """Get refresh token by id.""" if self._users is None: await self._async_load() @@ -259,7 +261,7 @@ class AuthStore: async def async_get_refresh_token_by_token( self, token: str - ) -> Optional[models.RefreshToken]: + ) -> models.RefreshToken | None: """Get refresh token by token.""" if self._users is None: await self._async_load() @@ -276,7 +278,7 @@ class AuthStore: @callback def async_log_refresh_token_usage( - self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None + self, refresh_token: models.RefreshToken, remote_ip: str | None = None ) -> None: """Update refresh token last used information.""" refresh_token.last_used_at = dt_util.utcnow() @@ -309,9 +311,9 @@ class AuthStore: self._set_defaults() return - users: Dict[str, models.User] = OrderedDict() - groups: Dict[str, models.Group] = OrderedDict() - credentials: Dict[str, models.Credentials] = OrderedDict() + users: dict[str, models.User] = OrderedDict() + groups: dict[str, models.Group] = OrderedDict() + credentials: dict[str, models.Credentials] = OrderedDict() # Soft-migrating data as we load. We are going to make sure we have a # read only group and an admin group. There are two states that we can @@ -328,7 +330,7 @@ class AuthStore: # was added. for group_dict in data.get("groups", []): - policy: Optional[PolicyType] = None + policy: PolicyType | None = None if group_dict["id"] == GROUP_ID_ADMIN: has_admin_group = True @@ -489,7 +491,7 @@ class AuthStore: self._store.async_delay_save(self._data_to_save, 1) @callback - def _data_to_save(self) -> Dict: + def _data_to_save(self) -> dict: """Return the data to store.""" assert self._users is not None assert self._groups is not None @@ -508,7 +510,7 @@ class AuthStore: groups = [] for group in self._groups.values(): - g_dict: Dict[str, Any] = { + g_dict: dict[str, Any] = { "id": group.id, # Name not read for sys groups. Kept here for backwards compat "name": group.name, @@ -567,7 +569,7 @@ class AuthStore: """Set default values for auth store.""" self._users = OrderedDict() - groups: Dict[str, models.Group] = OrderedDict() + groups: dict[str, models.Group] = OrderedDict() admin_group = _system_admin_group() groups[admin_group.id] = admin_group user_group = _system_user_group() diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index f29f5f8fcc2..d6989b6416f 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import importlib import logging import types -from typing import Any, Dict, Optional +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error @@ -38,7 +38,7 @@ class MultiFactorAuthModule: DEFAULT_TITLE = "Unnamed auth module" MAX_RETRY_TIME = 3 - def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize an auth module.""" self.hass = hass self.config = config @@ -87,7 +87,7 @@ class MultiFactorAuthModule: """Return whether user is setup.""" raise NotImplementedError - async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: """Return True if validation passed.""" raise NotImplementedError @@ -104,14 +104,14 @@ class SetupFlow(data_entry_flow.FlowHandler): self._user_id = user_id async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. Return self.async_create_entry(data={'result': result}) if finish. """ - errors: Dict[str, str] = {} + errors: dict[str, str] = {} if user_input: result = await self._auth_module.async_setup_user(self._user_id, user_input) @@ -125,7 +125,7 @@ class SetupFlow(data_entry_flow.FlowHandler): async def auth_mfa_module_from_config( - hass: HomeAssistant, config: Dict[str, Any] + hass: HomeAssistant, config: dict[str, Any] ) -> MultiFactorAuthModule: """Initialize an auth module from a config.""" module_name = config[CONF_TYPE] diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index ddceeaae826..1d40339417b 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -1,5 +1,7 @@ """Example auth module.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -28,7 +30,7 @@ class InsecureExampleModule(MultiFactorAuthModule): DEFAULT_TITLE = "Insecure Personal Identify Number" - def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) self._data = config["data"] @@ -75,17 +77,11 @@ class InsecureExampleModule(MultiFactorAuthModule): async def async_is_user_setup(self, user_id: str) -> bool: """Return whether user is setup.""" - for data in self._data: - if data["user_id"] == user_id: - return True - return False + return any(data["user_id"] == user_id for data in self._data) - async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: """Return True if validation passed.""" - for data in self._data: - if data["user_id"] == user_id: - # user_input has been validate in caller - if data["pin"] == user_input["pin"]: - return True - - return False + return any( + data["user_id"] == user_id and data["pin"] == user_input["pin"] + for data in self._data + ) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index c4e5800821e..76a5676d562 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -2,10 +2,12 @@ Sending HOTP through notify service """ +from __future__ import annotations + import asyncio from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict import attr import voluptuous as vol @@ -79,8 +81,8 @@ class NotifySetting: secret: str = attr.ib(factory=_generate_secret) # not persistent counter: int = attr.ib(factory=_generate_random) # not persistent - notify_service: Optional[str] = attr.ib(default=None) - target: Optional[str] = attr.ib(default=None) + notify_service: str | None = attr.ib(default=None) + target: str | None = attr.ib(default=None) _UsersDict = Dict[str, NotifySetting] @@ -92,10 +94,10 @@ class NotifyAuthModule(MultiFactorAuthModule): DEFAULT_TITLE = "Notify One-Time Password" - def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._user_settings: Optional[_UsersDict] = None + self._user_settings: _UsersDict | None = None self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -146,7 +148,7 @@ class NotifyAuthModule(MultiFactorAuthModule): ) @callback - def aync_get_available_notify_services(self) -> List[str]: + def aync_get_available_notify_services(self) -> list[str]: """Return list of notify services.""" unordered_services = set() @@ -198,7 +200,7 @@ class NotifyAuthModule(MultiFactorAuthModule): return user_id in self._user_settings - async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: """Return True if validation passed.""" if self._user_settings is None: await self._async_load() @@ -258,7 +260,7 @@ class NotifyAuthModule(MultiFactorAuthModule): ) async def async_notify( - self, code: str, notify_service: str, target: Optional[str] = None + self, code: str, notify_service: str, target: str | None = None ) -> None: """Send code by notify service.""" data = {"message": self._message_template.format(code)} @@ -276,23 +278,23 @@ class NotifySetupFlow(SetupFlow): auth_module: NotifyAuthModule, setup_schema: vol.Schema, user_id: str, - available_notify_services: List[str], + available_notify_services: list[str], ) -> None: """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user_id) # to fix typing complaint self._auth_module: NotifyAuthModule = auth_module self._available_notify_services = available_notify_services - self._secret: Optional[str] = None - self._count: Optional[int] = None - self._notify_service: Optional[str] = None - self._target: Optional[str] = None + self._secret: str | None = None + self._count: int | None = None + self._notify_service: str | None = None + self._target: str | None = None async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Let user select available notify services.""" - errors: Dict[str, str] = {} + errors: dict[str, str] = {} hass = self._auth_module.hass if user_input: @@ -306,7 +308,7 @@ class NotifySetupFlow(SetupFlow): if not self._available_notify_services: return self.async_abort(reason="no_available_service") - schema: Dict[str, Any] = OrderedDict() + schema: dict[str, Any] = OrderedDict() schema["notify_service"] = vol.In(self._available_notify_services) schema["target"] = vol.Optional(str) @@ -315,10 +317,10 @@ class NotifySetupFlow(SetupFlow): ) async def async_step_setup( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Verify user can receive one-time password.""" - errors: Dict[str, str] = {} + errors: dict[str, str] = {} hass = self._auth_module.hass if user_input: diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 4a6faef96c0..d20c8465546 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,7 +1,9 @@ """Time-based One Time Password auth module.""" +from __future__ import annotations + import asyncio from io import BytesIO -from typing import Any, Dict, Optional, Tuple +from typing import Any import voluptuous as vol @@ -50,7 +52,7 @@ def _generate_qr_code(data: str) -> str: ) -def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]: +def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]: """Generate a secret, url, and QR code.""" import pyotp # pylint: disable=import-outside-toplevel @@ -69,10 +71,10 @@ class TotpAuthModule(MultiFactorAuthModule): DEFAULT_TITLE = "Time-based One Time Password" MAX_RETRY_TIME = 5 - def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._users: Optional[Dict[str, str]] = None + self._users: dict[str, str] | None = None self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -100,7 +102,7 @@ class TotpAuthModule(MultiFactorAuthModule): """Save data.""" await self._user_store.async_save({STORAGE_USERS: self._users}) - def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str: + def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: """Create a ota_secret for user.""" import pyotp # pylint: disable=import-outside-toplevel @@ -145,7 +147,7 @@ class TotpAuthModule(MultiFactorAuthModule): return user_id in self._users # type: ignore - async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool: + async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool: """Return True if validation passed.""" if self._users is None: await self._async_load() @@ -181,13 +183,13 @@ class TotpSetupFlow(SetupFlow): # to fix typing complaint self._auth_module: TotpAuthModule = auth_module self._user = user - self._ota_secret: Optional[str] = None + self._ota_secret: str | None = None self._url = None # type Optional[str] self._image = None # type Optional[str] async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -195,7 +197,7 @@ class TotpSetupFlow(SetupFlow): """ import pyotp # pylint: disable=import-outside-toplevel - errors: Dict[str, str] = {} + errors: dict[str, str] = {} if user_input: verified = await self.hass.async_add_executor_job( diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 4cc67b2ebd4..758bbdb78e2 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,7 +1,9 @@ """Auth models.""" +from __future__ import annotations + from datetime import datetime, timedelta import secrets -from typing import Dict, List, NamedTuple, Optional +from typing import NamedTuple import uuid import attr @@ -21,7 +23,7 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" class Group: """A group.""" - name: Optional[str] = attr.ib() + name: str | None = attr.ib() policy: perm_mdl.PolicyType = attr.ib() id: str = attr.ib(factory=lambda: uuid.uuid4().hex) system_generated: bool = attr.ib(default=False) @@ -31,24 +33,24 @@ class Group: class User: """A user.""" - name: Optional[str] = attr.ib() + name: str | None = attr.ib() perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False) id: str = attr.ib(factory=lambda: uuid.uuid4().hex) is_owner: bool = attr.ib(default=False) is_active: bool = attr.ib(default=False) system_generated: bool = attr.ib(default=False) - groups: List[Group] = attr.ib(factory=list, eq=False, order=False) + groups: list[Group] = attr.ib(factory=list, eq=False, order=False) # List of credentials of a user. - credentials: List["Credentials"] = attr.ib(factory=list, eq=False, order=False) + credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False) # Tokens associated with a user. - refresh_tokens: Dict[str, "RefreshToken"] = attr.ib( + refresh_tokens: dict[str, RefreshToken] = attr.ib( factory=dict, eq=False, order=False ) - _permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib( + _permissions: perm_mdl.PolicyPermissions | None = attr.ib( init=False, eq=False, order=False, @@ -89,10 +91,10 @@ class RefreshToken: """RefreshToken for a user to grant new access tokens.""" user: User = attr.ib() - client_id: Optional[str] = attr.ib() + client_id: str | None = attr.ib() access_token_expiration: timedelta = attr.ib() - client_name: Optional[str] = attr.ib(default=None) - client_icon: Optional[str] = attr.ib(default=None) + client_name: str | None = attr.ib(default=None) + client_icon: str | None = attr.ib(default=None) token_type: str = attr.ib( default=TOKEN_TYPE_NORMAL, validator=attr.validators.in_( @@ -104,12 +106,12 @@ class RefreshToken: token: str = attr.ib(factory=lambda: secrets.token_hex(64)) jwt_key: str = attr.ib(factory=lambda: secrets.token_hex(64)) - last_used_at: Optional[datetime] = attr.ib(default=None) - last_used_ip: Optional[str] = attr.ib(default=None) + last_used_at: datetime | None = attr.ib(default=None) + last_used_ip: str | None = attr.ib(default=None) - credential: Optional["Credentials"] = attr.ib(default=None) + credential: Credentials | None = attr.ib(default=None) - version: Optional[str] = attr.ib(default=__version__) + version: str | None = attr.ib(default=__version__) @attr.s(slots=True) @@ -117,7 +119,7 @@ class Credentials: """Credentials for a user on an auth provider.""" auth_provider_type: str = attr.ib() - auth_provider_id: Optional[str] = attr.ib() + auth_provider_id: str | None = attr.ib() # Allow the auth provider to store data to represent their auth. data: dict = attr.ib() @@ -129,5 +131,5 @@ class Credentials: class UserMeta(NamedTuple): """User metadata.""" - name: Optional[str] + name: str | None is_active: bool diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 2f887d21b02..28ff3f638d4 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -1,6 +1,8 @@ """Permissions for Home Assistant.""" +from __future__ import annotations + import logging -from typing import Any, Callable, Optional +from typing import Any, Callable import voluptuous as vol @@ -19,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) class AbstractPermissions: """Default permissions class.""" - _cached_entity_func: Optional[Callable[[str, str], bool]] = None + _cached_entity_func: Callable[[str, str], bool] | None = None def _entity_func(self) -> Callable[[str, str], bool]: """Return a function that can test entity access.""" diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index be30c7bf69a..f19590a6349 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -1,6 +1,8 @@ """Entity permissions.""" +from __future__ import annotations + from collections import OrderedDict -from typing import Callable, Optional +from typing import Callable import voluptuous as vol @@ -43,14 +45,14 @@ ENTITY_POLICY_SCHEMA = vol.Any( def _lookup_domain( perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str -) -> Optional[ValueType]: +) -> ValueType | None: """Look up entity permissions by domain.""" return domains_dict.get(entity_id.split(".", 1)[0]) def _lookup_area( perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str -) -> Optional[ValueType]: +) -> ValueType | None: """Look up entity permissions by area.""" entity_entry = perm_lookup.entity_registry.async_get(entity_id) @@ -67,7 +69,7 @@ def _lookup_area( def _lookup_device( perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str -) -> Optional[ValueType]: +) -> ValueType | None: """Look up entity permissions by device.""" entity_entry = perm_lookup.entity_registry.async_get(entity_id) @@ -79,7 +81,7 @@ def _lookup_device( def _lookup_entity_id( perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str -) -> Optional[ValueType]: +) -> ValueType | None: """Look up entity permission by entity id.""" return entities_dict.get(entity_id) diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index fad98b3f22a..121d87f7848 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,13 +1,15 @@ """Merging of policies.""" -from typing import Dict, List, Set, cast +from __future__ import annotations + +from typing import cast from .types import CategoryType, PolicyType -def merge_policies(policies: List[PolicyType]) -> PolicyType: +def merge_policies(policies: list[PolicyType]) -> PolicyType: """Merge policies.""" - new_policy: Dict[str, CategoryType] = {} - seen: Set[str] = set() + new_policy: dict[str, CategoryType] = {} + seen: set[str] = set() for policy in policies: for category in policy: if category in seen: @@ -20,7 +22,7 @@ def merge_policies(policies: List[PolicyType]) -> PolicyType: return new_policy -def _merge_policies(sources: List[CategoryType]) -> CategoryType: +def _merge_policies(sources: list[CategoryType]) -> CategoryType: """Merge a policy.""" # When merging policies, the most permissive wins. # This means we order it like this: @@ -34,7 +36,7 @@ def _merge_policies(sources: List[CategoryType]) -> CategoryType: # merge each key in the source. policy: CategoryType = None - seen: Set[str] = set() + seen: set[str] = set() for source in sources: if source is None: continue diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 2542be14cc6..aa1a777ced2 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -1,11 +1,12 @@ """Models for permissions.""" +from __future__ import annotations + from typing import TYPE_CHECKING import attr if TYPE_CHECKING: - # pylint: disable=unused-import - from homeassistant.helpers import ( # noqa: F401 + from homeassistant.helpers import ( device_registry as dev_reg, entity_registry as ent_reg, ) @@ -15,5 +16,5 @@ if TYPE_CHECKING: class PermissionLookup: """Class to hold data for permission lookups.""" - entity_registry: "ent_reg.EntityRegistry" = attr.ib() - device_registry: "dev_reg.DeviceRegistry" = attr.ib() + entity_registry: ent_reg.EntityRegistry = attr.ib() + device_registry: dev_reg.DeviceRegistry = attr.ib() diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 11bbd878eb2..e95e0080b50 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,6 +1,8 @@ """Helpers to deal with permissions.""" +from __future__ import annotations + from functools import wraps -from typing import Callable, Dict, List, Optional, cast +from typing import Callable, Dict, Optional, cast from .const import SUBCAT_ALL from .models import PermissionLookup @@ -45,7 +47,7 @@ def compile_policy( assert isinstance(policy, dict) - funcs: List[Callable[[str, str], Optional[bool]]] = [] + funcs: list[Callable[[str, str], bool | None]] = [] for key, lookup_func in subcategories.items(): lookup_value = policy.get(key) @@ -80,10 +82,10 @@ def compile_policy( def _gen_dict_test_func( perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict -) -> Callable[[str, str], Optional[bool]]: +) -> Callable[[str, str], bool | None]: """Generate a lookup function.""" - def test_value(object_id: str, key: str) -> Optional[bool]: + def test_value(object_id: str, key: str) -> bool | None: """Test if permission is allowed based on the keys.""" schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 2afe1333c6a..6e188be1ffc 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import importlib import logging import types -from typing import Any, Dict, List, Optional +from typing import Any import voluptuous as vol from voluptuous.humanize import humanize_error @@ -42,7 +42,7 @@ class AuthProvider: DEFAULT_TITLE = "Unnamed auth provider" def __init__( - self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] + self, hass: HomeAssistant, store: AuthStore, config: dict[str, Any] ) -> None: """Initialize an auth provider.""" self.hass = hass @@ -50,7 +50,7 @@ class AuthProvider: self.config = config @property - def id(self) -> Optional[str]: + def id(self) -> str | None: """Return id of the auth provider. Optional, can be None. @@ -72,7 +72,7 @@ class AuthProvider: """Return whether multi-factor auth supported by the auth provider.""" return True - async def async_credentials(self) -> List[Credentials]: + async def async_credentials(self) -> list[Credentials]: """Return all credentials of this provider.""" users = await self.store.async_get_users() return [ @@ -86,7 +86,7 @@ class AuthProvider: ] @callback - def async_create_credentials(self, data: Dict[str, str]) -> Credentials: + def async_create_credentials(self, data: dict[str, str]) -> Credentials: """Create credentials.""" return Credentials( auth_provider_type=self.type, auth_provider_id=self.id, data=data @@ -94,7 +94,7 @@ class AuthProvider: # Implement by extending class - async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return the data flow for logging in with auth provider. Auth provider should extend LoginFlow and return an instance. @@ -102,7 +102,7 @@ class AuthProvider: raise NotImplementedError async def async_get_or_create_credentials( - self, flow_result: Dict[str, str] + self, flow_result: dict[str, str] ) -> Credentials: """Get credentials based on the flow result.""" raise NotImplementedError @@ -121,7 +121,7 @@ class AuthProvider: @callback def async_validate_refresh_token( - self, refresh_token: RefreshToken, remote_ip: Optional[str] = None + self, refresh_token: RefreshToken, remote_ip: str | None = None ) -> None: """Verify a refresh token is still valid. @@ -131,7 +131,7 @@ class AuthProvider: async def auth_provider_from_config( - hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] + hass: HomeAssistant, store: AuthStore, config: dict[str, Any] ) -> AuthProvider: """Initialize an auth provider from a config.""" provider_name = config[CONF_TYPE] @@ -188,17 +188,17 @@ class LoginFlow(data_entry_flow.FlowHandler): def __init__(self, auth_provider: AuthProvider) -> None: """Initialize the login flow.""" self._auth_provider = auth_provider - self._auth_module_id: Optional[str] = None + self._auth_module_id: str | None = None self._auth_manager = auth_provider.hass.auth - self.available_mfa_modules: Dict[str, str] = {} + self.available_mfa_modules: dict[str, str] = {} self.created_at = dt_util.utcnow() self.invalid_mfa_times = 0 - self.user: Optional[User] = None - self.credential: Optional[Credentials] = None + self.user: User | None = None + self.credential: Credentials | None = None async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -207,8 +207,8 @@ class LoginFlow(data_entry_flow.FlowHandler): raise NotImplementedError async def async_step_select_mfa_module( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of select mfa module.""" errors = {} @@ -232,8 +232,8 @@ class LoginFlow(data_entry_flow.FlowHandler): ) async def async_step_mfa( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of mfa validation.""" assert self.credential assert self.user @@ -273,7 +273,7 @@ class LoginFlow(data_entry_flow.FlowHandler): if not errors: return await self.async_finish(self.credential) - description_placeholders: Dict[str, Optional[str]] = { + description_placeholders: dict[str, str | None] = { "mfa_module_name": auth_module.name, "mfa_module_id": auth_module.id, } @@ -285,6 +285,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) -> dict: """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 b2b19221979..47a56d87097 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -1,10 +1,11 @@ """Auth provider that validates credentials via an external command.""" +from __future__ import annotations import asyncio.subprocess import collections import logging import os -from typing import Any, Dict, Optional, cast +from typing import Any, cast import voluptuous as vol @@ -51,9 +52,9 @@ class CommandLineAuthProvider(AuthProvider): attributes provided by external programs. """ super().__init__(*args, **kwargs) - self._user_meta: Dict[str, Dict[str, Any]] = {} + self._user_meta: dict[str, dict[str, Any]] = {} - async def async_login_flow(self, context: Optional[dict]) -> LoginFlow: + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" return CommandLineLoginFlow(self) @@ -82,7 +83,7 @@ class CommandLineAuthProvider(AuthProvider): raise InvalidAuthError if self.config[CONF_META]: - meta: Dict[str, str] = {} + meta: dict[str, str] = {} for _line in stdout.splitlines(): try: line = _line.decode().lstrip() @@ -99,7 +100,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: dict[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -125,8 +126,8 @@ class CommandLineLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of the form.""" errors = {} @@ -143,7 +144,7 @@ class CommandLineLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema: Dict[str, type] = collections.OrderedDict() + schema: dict[str, type] = collections.OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index c66ffa7332e..54d82013a75 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -5,7 +5,7 @@ import asyncio import base64 from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, Set, cast +from typing import Any, cast import bcrypt import voluptuous as vol @@ -21,7 +21,7 @@ STORAGE_VERSION = 1 STORAGE_KEY = "auth_provider.homeassistant" -def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]: +def _disallow_id(conf: dict[str, Any]) -> dict[str, Any]: """Disallow ID in config.""" if CONF_ID in conf: raise vol.Invalid("ID is not allowed for the homeassistant auth provider.") @@ -62,7 +62,7 @@ class Data: self._store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) - self._data: Optional[Dict[str, Any]] = None + self._data: dict[str, Any] | None = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. # Remove in 2020 or when we launch 1.0. @@ -83,7 +83,7 @@ class Data: if data is None: data = {"users": []} - seen: Set[str] = set() + seen: set[str] = set() for user in data["users"]: username = user["username"] @@ -121,7 +121,7 @@ class Data: self._data = data @property - def users(self) -> List[Dict[str, str]]: + def users(self) -> list[dict[str, str]]: """Return users.""" return self._data["users"] # type: ignore @@ -220,7 +220,7 @@ class HassAuthProvider(AuthProvider): def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize an Home Assistant auth provider.""" super().__init__(*args, **kwargs) - self.data: Optional[Data] = None + self.data: Data | None = None self._init_lock = asyncio.Lock() async def async_initialize(self) -> None: @@ -233,7 +233,7 @@ class HassAuthProvider(AuthProvider): await data.async_load() self.data = data - async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" return HassLoginFlow(self) @@ -277,7 +277,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: dict[str, str] ) -> Credentials: """Get credentials based on the flow result.""" if self.data is None: @@ -318,8 +318,8 @@ class HassLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of the form.""" errors = {} @@ -335,7 +335,7 @@ class HassLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema: Dict[str, type] = OrderedDict() + schema: dict[str, type] = OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 70014a236cd..c938a6fac81 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -1,7 +1,9 @@ """Example auth provider.""" +from __future__ import annotations + from collections import OrderedDict import hmac -from typing import Any, Dict, Optional, cast +from typing import Any, cast import voluptuous as vol @@ -33,7 +35,7 @@ class InvalidAuthError(HomeAssistantError): class ExampleAuthProvider(AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" - async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" return ExampleLoginFlow(self) @@ -60,7 +62,7 @@ class ExampleAuthProvider(AuthProvider): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: Dict[str, str] + self, flow_result: dict[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -94,8 +96,8 @@ class ExampleLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of the form.""" errors = {} @@ -111,7 +113,7 @@ class ExampleLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema: Dict[str, type] = OrderedDict() + schema: dict[str, type] = OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index ba96fa285f1..522751c70d6 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -3,8 +3,10 @@ Support Legacy API password auth provider. It will be removed when auth system production ready """ +from __future__ import annotations + import hmac -from typing import Any, Dict, Optional, cast +from typing import Any, cast import voluptuous as vol @@ -40,7 +42,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): """Return api_password.""" return str(self.config[CONF_API_PASSWORD]) - async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" return LegacyLoginFlow(self) @@ -55,7 +57,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: Dict[str, str] + self, flow_result: dict[str, str] ) -> Credentials: """Return credentials for this login.""" credentials = await self.async_credentials() @@ -79,8 +81,8 @@ class LegacyLoginFlow(LoginFlow): """Handler for the login flow.""" async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 2afdbf98196..85b43d89f3f 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,6 +3,8 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ +from __future__ import annotations + from ipaddress import ( IPv4Address, IPv4Network, @@ -11,7 +13,7 @@ from ipaddress import ( ip_address, ip_network, ) -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Union, cast import voluptuous as vol @@ -68,12 +70,12 @@ class TrustedNetworksAuthProvider(AuthProvider): DEFAULT_TITLE = "Trusted Networks" @property - def trusted_networks(self) -> List[IPNetwork]: + def trusted_networks(self) -> list[IPNetwork]: """Return trusted networks.""" return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS]) @property - def trusted_users(self) -> Dict[IPNetwork, Any]: + def trusted_users(self) -> dict[IPNetwork, Any]: """Return trusted users per network.""" return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS]) @@ -82,7 +84,7 @@ class TrustedNetworksAuthProvider(AuthProvider): """Trusted Networks auth provider does not support MFA.""" return False - async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow: + async def async_login_flow(self, context: dict | None) -> LoginFlow: """Return a flow to login.""" assert context is not None ip_addr = cast(IPAddress, context.get("ip_address")) @@ -111,7 +113,7 @@ class TrustedNetworksAuthProvider(AuthProvider): if ( user.id in user_list or any( - [group.id in flattened_group_list for group in user.groups] + group.id in flattened_group_list for group in user.groups ) ) ] @@ -125,7 +127,7 @@ class TrustedNetworksAuthProvider(AuthProvider): ) async def async_get_or_create_credentials( - self, flow_result: Dict[str, str] + self, flow_result: dict[str, str] ) -> Credentials: """Get credentials based on the flow result.""" user_id = flow_result["user"] @@ -169,7 +171,7 @@ class TrustedNetworksAuthProvider(AuthProvider): @callback def async_validate_refresh_token( - self, refresh_token: RefreshToken, remote_ip: Optional[str] = None + self, refresh_token: RefreshToken, remote_ip: str | None = None ) -> None: """Verify a refresh token is still valid.""" if remote_ip is None: @@ -186,7 +188,7 @@ class TrustedNetworksLoginFlow(LoginFlow): self, auth_provider: TrustedNetworksAuthProvider, ip_addr: IPAddress, - available_users: Dict[str, Optional[str]], + available_users: dict[str, str | None], allow_bypass_login: bool, ) -> None: """Initialize the login flow.""" @@ -196,8 +198,8 @@ class TrustedNetworksLoginFlow(LoginFlow): self._allow_bypass_login = allow_bypass_login async def async_step_init( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Handle the step of the form.""" try: cast( diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index fd2d580a879..d19ddaf4f5d 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,4 +1,6 @@ """Provide methods to bootstrap a Home Assistant instance.""" +from __future__ import annotations + import asyncio import contextlib from datetime import datetime @@ -8,7 +10,7 @@ import os import sys import threading from time import monotonic -from typing import TYPE_CHECKING, Any, Dict, Optional, Set +from typing import TYPE_CHECKING, Any import voluptuous as vol import yarl @@ -28,7 +30,6 @@ from homeassistant.setup import ( from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.package import async_get_user_site, is_virtual_env -from homeassistant.util.yaml import clear_secret_cache if TYPE_CHECKING: from .runner import RuntimeConfig @@ -75,8 +76,8 @@ STAGE_1_INTEGRATIONS = { async def async_setup_hass( - runtime_config: "RuntimeConfig", -) -> Optional[core.HomeAssistant]: + runtime_config: RuntimeConfig, +) -> core.HomeAssistant | None: """Set up Home Assistant.""" hass = core.HomeAssistant() hass.config.config_dir = runtime_config.config_dir @@ -122,8 +123,6 @@ async def async_setup_hass( basic_setup_success = ( await async_from_config_dict(config_dict, hass) is not None ) - finally: - clear_secret_cache() if config_dict is None: safe_mode = True @@ -191,7 +190,7 @@ def open_hass_ui(hass: core.HomeAssistant) -> None: async def async_from_config_dict( config: ConfigType, hass: core.HomeAssistant -) -> Optional[core.HomeAssistant]: +) -> core.HomeAssistant | None: """Try to configure Home Assistant from a configuration dictionary. Dynamically loads required components and its dependencies. @@ -258,8 +257,8 @@ async def async_from_config_dict( def async_enable_logging( hass: core.HomeAssistant, verbose: bool = False, - log_rotate_days: Optional[int] = None, - log_file: Optional[str] = None, + log_rotate_days: int | None = None, + log_file: str | None = None, log_no_color: bool = False, ) -> None: """Set up the logging. @@ -365,7 +364,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str: @core.callback -def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: +def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: """Get domains of components to set up.""" # Filter out the repeating and common config section [homeassistant] domains = {key.split(" ")[0] for key in config if key != core.DOMAIN} @@ -382,7 +381,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: async def _async_log_pending_setups( - hass: core.HomeAssistant, domains: Set[str], setup_started: Dict[str, datetime] + hass: core.HomeAssistant, domains: set[str], setup_started: dict[str, datetime] ) -> None: """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" while True: @@ -399,9 +398,9 @@ async def _async_log_pending_setups( async def async_setup_multi_components( hass: core.HomeAssistant, - domains: Set[str], - config: Dict[str, Any], - setup_started: Dict[str, datetime], + domains: set[str], + config: dict[str, Any], + setup_started: dict[str, datetime], ) -> None: """Set up multiple domains. Log on failure.""" futures = { @@ -425,7 +424,7 @@ async def async_setup_multi_components( async def _async_set_up_integrations( - hass: core.HomeAssistant, config: Dict[str, Any] + hass: core.HomeAssistant, config: dict[str, Any] ) -> None: """Set up all the integrations.""" setup_started = hass.data[DATA_SETUP_STARTED] = {} @@ -433,7 +432,7 @@ async def _async_set_up_integrations( # Resolve all dependencies so we know all integrations # that will have to be loaded and start rightaway - integration_cache: Dict[str, loader.Integration] = {} + integration_cache: dict[str, loader.Integration] = {} to_resolve = domains_to_setup while to_resolve: old_to_resolve = to_resolve diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 20c0624742c..c1c89951c3f 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -66,7 +66,7 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) -ABODE_PLATFORMS = [ +PLATFORMS = [ "alarm_control_panel", "binary_sensor", "lock", @@ -138,7 +138,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = AbodeSystem(abode, polling) - for platform in ABODE_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) ) @@ -158,7 +158,7 @@ async def async_unload_entry(hass, config_entry): tasks = [] - for platform in ABODE_PLATFORMS: + for platform in PLATFORMS: tasks.append( hass.config_entries.async_forward_entry_unload(config_entry, platform) ) @@ -363,7 +363,7 @@ class AbodeDevice(AbodeEntity): return self._device.name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -411,7 +411,7 @@ class AbodeAutomation(AbodeEntity): return self._automation.name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"} diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c508d0f0240..6d0c030e3e1 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): self._device.set_away() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 76c23f7f705..4e6a6f4c904 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST -from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import +from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER CONF_MFA = "mfa_code" CONF_POLLING = "polling" @@ -163,7 +163,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" if self._async_current_entries(): - LOGGER.warning("Already configured. Only a single configuration possible.") + LOGGER.warning("Already configured; Only a single configuration possible") return self.async_abort(reason="single_instance_allowed") self._polling = import_config.get(CONF_POLLING, False) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 6ecc5c871cd..e3ececb62d9 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,6 +1,7 @@ """Support for Abode Security System sensors.""" import abodepy.helpers.constants as CONST +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class AbodeSensor(AbodeDevice): +class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" def __init__(self, data, device, sensor_type): diff --git a/homeassistant/components/abode/translations/fa.json b/homeassistant/components/abode/translations/fa.json new file mode 100644 index 00000000000..4ceaaf32a13 --- /dev/null +++ b/homeassistant/components/abode/translations/fa.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u06a9\u0644\u0645\u0647 \u0639\u0628\u0648\u0631", + "username": "\u0627\u06cc\u0645\u06cc\u0644" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/he.json b/homeassistant/components/abode/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/abode/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index 5df508d0f33..260416b07bb 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -1,16 +1,25 @@ { "config": { "abort": { - "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d" }, "step": { "mfa": { "data": { "mfa_code": "MFA k\u00f3d (6 jegy\u0171)" + }, + "title": "Add meg az Abode MFA k\u00f3dj\u00e1t" + }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" } }, "user": { diff --git a/homeassistant/components/abode/translations/id.json b/homeassistant/components/abode/translations/id.json new file mode 100644 index 00000000000..2dc79c833b2 --- /dev/null +++ b/homeassistant/components/abode/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_mfa_code": "Kode MFA tidak valid" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "Kode MFA (6 digit)" + }, + "title": "Masukkan kode MFA Anda untuk Abode" + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Masukkan informasi masuk Abode Anda" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Masukkan informasi masuk Abode Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/ko.json b/homeassistant/components/abode/translations/ko.json index a9756447adf..85d3ef81aeb 100644 --- a/homeassistant/components/abode/translations/ko.json +++ b/homeassistant/components/abode/translations/ko.json @@ -2,18 +2,26 @@ "config": { "abort": { "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "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_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_mfa_code": "MFA \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA \ucf54\ub4dc (6\uc790\ub9ac)" + }, + "title": "Abode\uc5d0 \ub300\ud55c MFA \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + }, "reauth_confirm": { "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c" - } + }, + "title": "Abode \ub85c\uadf8\uc778 \uc815\ubcf4 \uc785\ub825\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 27dbae7a41f..4ed471a50f5 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -8,8 +8,6 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from homeassistant.const import CONF_API_KEY -from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,12 +24,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "weather"] -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured AccuWeather.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, config_entry) -> bool: """Set up AccuWeather as config entry.""" api_key = config_entry.data[CONF_API_KEY] @@ -45,21 +37,18 @@ async def async_setup_entry(hass, config_entry) -> bool: coordinator = AccuWeatherDataUpdateCoordinator( hass, websession, api_key, location_key, forecast ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() undo_listener = config_entry.add_update_listener(update_listener) - hass.data[DOMAIN][config_entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -70,8 +59,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index 03d6f40181c..6dac2aee286 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import +from .const import CONF_FORECAST, DOMAIN class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index b03c0e51018..fd91f62ae33 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.1.0"], + "requirements": ["accuweather==0.1.1"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum" diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 90058e254dc..722dd8869be 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,4 +1,5 @@ """Support for the AccuWeather service.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -48,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class AccuWeatherSensor(CoordinatorEntity): +class AccuWeatherSensor(CoordinatorEntity, SensorEntity): """Define an AccuWeather entity.""" def __init__(self, name, kind, coordinator, forecast_day=None): @@ -141,7 +142,7 @@ class AccuWeatherSensor(CoordinatorEntity): return SENSOR_TYPES[self.kind][self._unit_system] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.forecast_day is not None: if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 814e57d1d6c..330f2850d26 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Sie m\u00fcssen warten oder den API-Schl\u00fcssel \u00e4ndern." }, "step": { "user": { diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/accuweather/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json new file mode 100644 index 00000000000..8a0f7f5a198 --- /dev/null +++ b/homeassistant/components/accuweather/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/id.json b/homeassistant/components/accuweather/translations/id.json new file mode 100644 index 00000000000..970b3a026b7 --- /dev/null +++ b/homeassistant/components/accuweather/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "requests_exceeded": "Jumlah permintaan yang diizinkan ke API Accuweather telah terlampaui. Anda harus menunggu atau mengubah Kunci API." + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/accuweather/\n\nBeberapa sensor tidak diaktifkan secara default. Anda dapat mengaktifkannya di registri entitas setelah konfigurasi integrasi.\nPrakiraan cuaca tidak diaktifkan secara default. Anda dapat mengaktifkannya di opsi integrasi.", + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Prakiraan cuaca" + }, + "description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit.", + "title": "Opsi AccuWeather" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server AccuWeather", + "remaining_requests": "Sisa permintaan yang diizinkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/ko.json b/homeassistant/components/accuweather/translations/ko.json index 2f0a01e094b..d992d0bfdd4 100644 --- a/homeassistant/components/accuweather/translations/ko.json +++ b/homeassistant/components/accuweather/translations/ko.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "requests_exceeded": "Accuweather API\uc5d0 \ud5c8\uc6a9\ub41c \uc694\uccad \uc218\uac00 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\ub2e4\ub9ac\uac70\ub098 API \ud0a4\ub97c \ubcc0\uacbd\ud574\uc57c \ud569\ub2c8\ub2e4." }, "step": { "user": { @@ -15,15 +16,26 @@ "longitude": "\uacbd\ub3c4", "name": "\uc774\ub984" }, - "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "AccuWeather" } } }, "options": { "step": { "user": { - "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4." + "data": { + "forecast": "\ub0a0\uc528 \uc608\ubcf4" + }, + "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4.", + "title": "AccuWeather \uc635\uc158" } } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather \uc11c\ubc84 \uc5f0\uacb0", + "remaining_requests": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json index 4bf5f9fce45..f04d93b5921 100644 --- a/homeassistant/components/accuweather/translations/nl.json +++ b/homeassistant/components/accuweather/translations/nl.json @@ -31,5 +31,11 @@ "title": "AccuWeather-opties" } } + }, + "system_health": { + "info": { + "can_reach_server": "Kan AccuWeather server bereiken", + "remaining_requests": "Resterende toegestane verzoeken" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.hu.json b/homeassistant/components/accuweather/translations/sensor.hu.json new file mode 100644 index 00000000000..49f2fe41ab3 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Cs\u00f6kken\u0151", + "rising": "Emelked\u0151", + "steady": "\u00c1lland\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.id.json b/homeassistant/components/accuweather/translations/sensor.id.json new file mode 100644 index 00000000000..8ce99bbc8c3 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.id.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Turun", + "rising": "Naik", + "steady": "Tetap" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.ko.json b/homeassistant/components/accuweather/translations/sensor.ko.json new file mode 100644 index 00000000000..287974fa3fd --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.ko.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\ud558\uac15", + "rising": "\uc0c1\uc2b9", + "steady": "\uc548\uc815" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 101f7cbd615..4a61ec793db 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -132,7 +132,7 @@ class AcerSwitch(SwitchEntity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return state attributes.""" return self._attributes diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index 3b4f135a6fd..926208fba40 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -11,11 +11,6 @@ CONF_HUBS = "hubs" PLATFORMS = ["cover", "sensor"] -async def async_setup(hass: core.HomeAssistant, config: dict): - """Set up the Rollease Acmeda Automate component.""" - return True - - async def async_setup_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): @@ -28,9 +23,9 @@ async def async_setup_entry( hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = hub - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -45,8 +40,8 @@ async def async_unload_entry( unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index f421fa9ca25..70935b70869 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Rollease Acmeda Automate Pulse Hub.""" +from __future__ import annotations + import asyncio -from typing import Dict, Optional +from contextlib import suppress import aiopulse import async_timeout @@ -8,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -19,7 +21,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self.discovered_hubs: Optional[Dict[str, aiopulse.Hub]] = None + self.discovered_hubs: dict[str, aiopulse.Hub] | None = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -36,15 +38,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } hubs = [] - try: - with async_timeout.timeout(5): + with suppress(asyncio.TimeoutError): + async with async_timeout.timeout(5): async for hub in aiopulse.Hub.discover(): if hub.id not in already_configured: hubs.append(hub) - except asyncio.TimeoutError: - pass - if len(hubs) == 0: + if not hubs: return self.async_abort(reason="no_devices_found") if len(hubs) == 1: diff --git a/homeassistant/components/acmeda/hub.py b/homeassistant/components/acmeda/hub.py index 0b74b874dcc..e156ee5cb78 100644 --- a/homeassistant/components/acmeda/hub.py +++ b/homeassistant/components/acmeda/hub.py @@ -1,6 +1,7 @@ """Code to handle a Pulse Hub.""" +from __future__ import annotations + import asyncio -from typing import Optional import aiopulse @@ -17,7 +18,7 @@ class PulseHub: """Initialize the system.""" self.config_entry = config_entry self.hass = hass - self.api: Optional[aiopulse.Hub] = None + self.api: aiopulse.Hub | None = None self.tasks = [] self.current_rollers = {} self.cleanup_callbacks = [] diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index f427548ab94..4f617c5726f 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -1,4 +1,5 @@ """Support for Acmeda Roller Blind Batteries.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -29,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class AcmedaBattery(AcmedaBase): +class AcmedaBattery(AcmedaBase, SensorEntity): """Representation of a Acmeda cover device.""" device_class = DEVICE_CLASS_BATTERY diff --git a/homeassistant/components/acmeda/translations/hu.json b/homeassistant/components/acmeda/translations/hu.json new file mode 100644 index 00000000000..6105977de80 --- /dev/null +++ b/homeassistant/components/acmeda/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/id.json b/homeassistant/components/acmeda/translations/id.json new file mode 100644 index 00000000000..6e80d134f5a --- /dev/null +++ b/homeassistant/components/acmeda/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "step": { + "user": { + "data": { + "id": "ID Host" + }, + "title": "Pilih hub untuk ditambahkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index e3fdeaf35f2..c88ed546b9d 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -53,7 +53,7 @@ class ActiontecDeviceScanner(DeviceScanner): self.last_results = [] data = self.get_actiontec_data() self.success_init = data is not None - _LOGGER.info("canner initialized") + _LOGGER.info("Scanner initialized") def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 71dff2ab6ee..0f10d20ec59 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,6 +1,8 @@ """Support for AdGuard Home.""" +from __future__ import annotations + import logging -from typing import Any, Dict +from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol @@ -27,11 +29,11 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -43,13 +45,10 @@ SERVICE_REFRESH_SCHEMA = vol.Schema( {vol.Optional(CONF_FORCE, default=False): cv.boolean} ) - -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the AdGuard Home components.""" - return True +PLATFORMS = ["sensor", "switch"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AdGuard Home from a config entry.""" session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) adguard = AdGuardHome( @@ -69,32 +68,36 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - for component in "sensor", "switch": + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) async def add_url(call) -> None: """Service call to add a new filter subscription to AdGuard Home.""" await adguard.filtering.add_url( - call.data.get(CONF_NAME), call.data.get(CONF_URL) + allowlist=False, name=call.data.get(CONF_NAME), url=call.data.get(CONF_URL) ) async def remove_url(call) -> None: """Service call to remove a filter subscription from AdGuard Home.""" - await adguard.filtering.remove_url(call.data.get(CONF_URL)) + await adguard.filtering.remove_url(allowlist=False, url=call.data.get(CONF_URL)) async def enable_url(call) -> None: """Service call to enable a filter subscription in AdGuard Home.""" - await adguard.filtering.enable_url(call.data.get(CONF_URL)) + await adguard.filtering.enable_url(allowlist=False, url=call.data.get(CONF_URL)) async def disable_url(call) -> None: """Service call to disable a filter subscription in AdGuard Home.""" - await adguard.filtering.disable_url(call.data.get(CONF_URL)) + await adguard.filtering.disable_url( + allowlist=False, url=call.data.get(CONF_URL) + ) async def refresh(call) -> None: """Service call to refresh the filter subscriptions in AdGuard Home.""" - await adguard.filtering.refresh(call.data.get(CONF_FORCE)) + await adguard.filtering.refresh( + allowlist=False, force=call.data.get(CONF_FORCE) + ) hass.services.async_register( DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA @@ -115,7 +118,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload AdGuard Home config entry.""" hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) @@ -123,8 +126,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - for component in "sensor", "switch": - await hass.config_entries.async_forward_entry_unload(entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, platform) del hass.data[DOMAIN] @@ -189,7 +192,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): """Defines a AdGuard Home device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this AdGuard Home instance.""" return { "identifiers": { diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index d728eed3003..d5ec79d788f 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,9 +1,12 @@ """Config flow to configure the AdGuard Home integration.""" +from __future__ import annotations + +from typing import Any + from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.adguard.const import DOMAIN from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -15,9 +18,10 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .const import DOMAIN -@config_entries.HANDLERS.register(DOMAIN) -class AdGuardHomeFlowHandler(ConfigFlow): + +class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a AdGuard Home config flow.""" VERSION = 1 @@ -25,7 +29,9 @@ class AdGuardHomeFlowHandler(ConfigFlow): _hassio_discovery = None - async def _show_setup_form(self, errors=None): + async def _show_setup_form( + self, errors: dict[str, str] | None = None + ) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -42,7 +48,9 @@ class AdGuardHomeFlowHandler(ConfigFlow): errors=errors or {}, ) - async def _show_hassio_form(self, errors=None): + async def _show_hassio_form( + self, errors: dict[str, str] | None = None + ) -> dict[str, Any]: """Show the Hass.io confirmation form to the user.""" return self.async_show_form( step_id="hassio_confirm", @@ -51,7 +59,9 @@ class AdGuardHomeFlowHandler(ConfigFlow): errors=errors or {}, ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -91,7 +101,7 @@ class AdGuardHomeFlowHandler(ConfigFlow): }, ) - async def async_step_hassio(self, discovery_info): + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, Any]: """Prepare configuration for a Hass.io AdGuard Home add-on. This flow is triggered by the discovery component. @@ -100,6 +110,7 @@ class AdGuardHomeFlowHandler(ConfigFlow): 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] @@ -129,7 +140,9 @@ class AdGuardHomeFlowHandler(ConfigFlow): return self.async_abort(reason="existing_instance_updated") - async def async_step_hassio_confirm(self, user_input=None): + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Confirm Hass.io discovery.""" if user_input is None: return await self._show_hassio_form() diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 0bcd25569a5..dd23e561364 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -3,6 +3,6 @@ "name": "AdGuard Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": ["adguardhome==0.4.2"], + "requirements": ["adguardhome==0.5.0"], "codeowners": ["@frenck"] } diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 05e23ba8b80..dd0400b6592 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -1,25 +1,29 @@ """Support for AdGuard Home sensors.""" +from __future__ import annotations + from datetime import timedelta +from typing import Callable -from adguardhome import AdGuardHomeConnectionError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError -from homeassistant.components.adguard import AdGuardHomeDeviceEntity -from homeassistant.components.adguard.const import ( - DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERION, - DOMAIN, -) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity import Entity + +from . import AdGuardHomeDeviceEntity +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: ConfigEntry, + 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] @@ -45,12 +49,12 @@ async def async_setup_entry( async_add_entities(sensors, True) -class AdGuardHomeSensor(AdGuardHomeDeviceEntity): +class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): """Defines a AdGuard Home sensor.""" def __init__( self, - adguard, + adguard: AdGuardHome, name: str, icon: str, measurement: str, @@ -78,12 +82,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity): ) @property - def state(self): + def state(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._unit_of_measurement @@ -91,7 +95,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity): class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): """Defines a AdGuard Home DNS Queries sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries" @@ -105,7 +109,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked by filtering sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -124,7 +128,7 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): """Defines a AdGuard Home blocked percentage sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -143,7 +147,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by parental control sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -161,7 +165,7 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe browsing sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -179,7 +183,7 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Defines a AdGuard Home replaced by safe search sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -197,7 +201,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): """Defines a AdGuard Home average processing time sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -216,7 +220,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): """Defines a AdGuard Home rules count sensor.""" - def __init__(self, adguard): + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home sensor.""" super().__init__( adguard, @@ -229,4 +233,4 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.rules_count() + self._state = await self.adguard.filtering.rules_count(allowlist=False) diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index 2d4bb49304f..4e6a63cfd3a 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -13,8 +13,8 @@ } }, "hassio_confirm": { - "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 Hass.io add-on: {addon}?" + "title": "AdGuard Home via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?" } }, "error": { diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 44aab11573d..0b127a280cf 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -1,19 +1,20 @@ """Support for AdGuard Home switches.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Callable -from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError -from homeassistant.components.adguard import AdGuardHomeDeviceEntity -from homeassistant.components.adguard.const import ( - DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERION, - DOMAIN, -) from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity import Entity + +from . import AdGuardHomeDeviceEntity +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -22,7 +23,9 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: ConfigEntry, + 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] @@ -49,8 +52,13 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Defines a AdGuard Home switch.""" def __init__( - self, adguard, name: str, icon: str, key: str, enabled_default: bool = True - ): + self, + adguard: AdGuardHome, + name: str, + icon: str, + key: str, + enabled_default: bool = True, + ) -> None: """Initialize AdGuard Home switch.""" self._state = False self._key = key @@ -96,7 +104,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home protection switch.""" - def __init__(self, adguard) -> None: + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, "AdGuard Protection", "mdi:shield-check", "protection" @@ -118,7 +126,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home parental control switch.""" - def __init__(self, adguard) -> None: + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, "AdGuard Parental Control", "mdi:shield-check", "parental" @@ -140,7 +148,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home safe search switch.""" - def __init__(self, adguard) -> None: + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch" @@ -162,7 +170,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home safe search switch.""" - def __init__(self, adguard) -> None: + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" @@ -184,7 +192,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home filtering switch.""" - def __init__(self, adguard) -> None: + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home switch.""" super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering") @@ -204,7 +212,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): """Defines a AdGuard Home query log switch.""" - def __init__(self, adguard) -> None: + def __init__(self, adguard: AdGuardHome) -> None: """Initialize AdGuard Home switch.""" super().__init__( adguard, diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 1edfc9d8da6..82c658e7c15 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?", - "title": "AdGuard Home \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?", + "title": "AdGuard Home \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index 422fde9479a..8c8086813aa 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -10,7 +10,7 @@ "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 (complement de Hass.io)" + "title": "AdGuard Home via complement de Hass.io" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json index 27b9d291fc2..00531088a08 100644 --- a/homeassistant/components/adguard/translations/cs.json +++ b/homeassistant/components/adguard/translations/cs.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?", - "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed Supervisor {addon}?", + "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/da.json b/homeassistant/components/adguard/translations/da.json index 927fd03d50a..79a1937eba8 100644 --- a/homeassistant/components/adguard/translations/da.json +++ b/homeassistant/components/adguard/translations/da.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?", - "title": "AdGuard Home via Hass.io-tilf\u00f8jelse" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Supervisor-tilf\u00f8jelsen: {addon}?", + "title": "AdGuard Home via Supervisor-tilf\u00f8jelse" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 67746b3abcf..2731f3f7eba 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?", - "title": "AdGuard Home \u00fcber das Hass.io Add-on" + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Supervisor-Add-On hergestellt wird: {addon}?", + "title": "AdGuard Home \u00fcber das Supervisor Add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json index 5efdfae1802..8fac53b61ab 100644 --- a/homeassistant/components/adguard/translations/es-419.json +++ b/homeassistant/components/adguard/translations/es-419.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Hass.io: {addon}?", - "title": "AdGuard Home a trav\u00e9s del complemento Hass.io" + "description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Supervisor: {addon}?", + "title": "AdGuard Home a trav\u00e9s del complemento Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index a165a9b1c09..3ffdb6b9eb0 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?", - "title": "AdGuard Home a trav\u00e9s del complemento Hass.io" + "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?", + "title": "AdGuard Home a trav\u00e9s del complemento Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/et.json b/homeassistant/components/adguard/translations/et.json index 3408d752522..800b7c37c49 100644 --- a/homeassistant/components/adguard/translations/et.json +++ b/homeassistant/components/adguard/translations/et.json @@ -10,7 +10,7 @@ "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 pistikprogrammi kaudu" + "title": "AdGuard Home Hass.io lisandmooduli abil" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/he.json b/homeassistant/components/adguard/translations/he.json index 49c18fac88c..1471fd6603b 100644 --- a/homeassistant/components/adguard/translations/he.json +++ b/homeassistant/components/adguard/translations/he.json @@ -4,6 +4,7 @@ "user": { "data": { "host": "Host", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "port": "\u05e4\u05d5\u05e8\u05d8" } } diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 3f67c765850..3813fae8f3c 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, @@ -9,7 +12,9 @@ "host": "Hoszt", "password": "Jelsz\u00f3", "port": "Port", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } } } diff --git a/homeassistant/components/adguard/translations/id.json b/homeassistant/components/adguard/translations/id.json index c5d61d91df0..d2e36cfe5b9 100644 --- a/homeassistant/components/adguard/translations/id.json +++ b/homeassistant/components/adguard/translations/id.json @@ -1,10 +1,27 @@ { "config": { + "abort": { + "existing_instance_updated": "Memperbarui konfigurasi yang ada.", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, "step": { + "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on Supervisor {addon}?", + "title": "AdGuard Home melalui add-on Home Assistant" + }, "user": { "data": { - "password": "Kata sandi" - } + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Siapkan instans AdGuard Home Anda untuk pemantauan dan kontrol." } } } diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index 3df01316aa1..3758e093547 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon}?", + "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" }, "user": { diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index b14e627ca56..8564ba19f3c 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -2,14 +2,14 @@ "config": { "abort": { "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 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "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?", + "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" }, "user": { diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json index 135451c061f..ae7e6ad99be 100644 --- a/homeassistant/components/adguard/translations/lb.json +++ b/homeassistant/components/adguard/translations/lb.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", - "title": "AdGuard Home via Hass.io add-on" + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?", + "title": "AdGuard Home via Supervisor add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 4c735333932..205193be8f8 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Hass.io-add-on: {addon}?", - "title": "AdGuard Home via Hass.io add-on" + "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" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index 25046c8d38f..11c35c5895e 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home gitt av Hass.io-tillegg {addon}?", + "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" }, "user": { diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index 41cb2c019dd..f5c433a0bf4 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -9,7 +9,7 @@ }, "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}?", + "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" }, "user": { diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json index ae8899bb1a7..959c7ba3638 100644 --- a/homeassistant/components/adguard/translations/pt-BR.json +++ b/homeassistant/components/adguard/translations/pt-BR.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Hass.io: {addon} ?", - "title": "AdGuard Home via add-on Hass.io" + "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Supervisor: {addon} ?", + "title": "AdGuard Home via add-on Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index 5d8abfc9f56..a7e494936b8 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "title": "AdGuard Home via Hass.io add-on" + "title": "AdGuard Home via Supervisor add-on" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 5e8483047f8..97dc6505c3b 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -18,7 +18,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home." diff --git a/homeassistant/components/adguard/translations/sl.json b/homeassistant/components/adguard/translations/sl.json index 06ad40a17d6..34b03263ceb 100644 --- a/homeassistant/components/adguard/translations/sl.json +++ b/homeassistant/components/adguard/translations/sl.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?", - "title": "AdGuard Home preko dodatka Hass.io" + "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Supervisor add-on {addon} ?", + "title": "AdGuard Home preko dodatka Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index 5b9a0f9a969..ca6158eaf32 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Hass.io Add-on: {addon}?", - "title": "AdGuard Home via Hass.io-till\u00e4gget" + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Supervisor Add-on: {addon}?", + "title": "AdGuard Home via Supervisor-till\u00e4gget" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/uk.json b/homeassistant/components/adguard/translations/uk.json index 8c24fb0a877..28d02f25b7e 100644 --- a/homeassistant/components/adguard/translations/uk.json +++ b/homeassistant/components/adguard/translations/uk.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index 8306b2daf70..250b2e0d891 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 AdGuard Home" + "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" }, "user": { "data": { diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 2f411b27723..933950dcf1b 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -2,7 +2,7 @@ import voluptuous as vol from homeassistant.components import ads -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([entity]) -class AdsSensor(AdsEntity): +class AdsSensor(AdsEntity, SensorEntity): """Representation of an ADS sensor entity.""" def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 7b270f1f335..98c6c401810 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -7,24 +7,17 @@ import logging from advantage_air import ApiError, advantage_air from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ADVANTAGE_AIR_RETRY, DOMAIN ADVANTAGE_AIR_SYNC_INTERVAL = 15 -ADVANTAGE_AIR_PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"] +PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up Advantage Air integration.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass, entry): """Set up Advantage Air config.""" ip_address = entry.data[CONF_IP_ADDRESS] @@ -57,17 +50,15 @@ async def async_setup_entry(hass, entry): except ApiError as err: _LOGGER.warning(err) - await coordinator.async_refresh() - - if not coordinator.data: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, "async_change": async_change, } - for platform in ADVANTAGE_AIR_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) @@ -80,8 +71,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in ADVANTAGE_AIR_PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index d25ce4888fc..19ac584ac2f 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for Advantage Air integration.""" import voluptuous as vol +from homeassistant.components.sensor import SensorEntity from homeassistant.const import PERCENTAGE from homeassistant.helpers import config_validation as cv, entity_platform @@ -40,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class AdvantageAirTimeTo(AdvantageAirEntity): +class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" def __init__(self, instance, ac_key, action): @@ -82,7 +83,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity): await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) -class AdvantageAirZoneVent(AdvantageAirEntity): +class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" @property @@ -115,7 +116,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity): return "mdi:fan-off" -class AdvantageAirZoneSignal(AdvantageAirEntity): +class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" @property diff --git a/homeassistant/components/advantage_air/translations/hu.json b/homeassistant/components/advantage_air/translations/hu.json index 0da6d0d5304..e82e88da8d2 100644 --- a/homeassistant/components/advantage_air/translations/hu.json +++ b/homeassistant/components/advantage_air/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, diff --git a/homeassistant/components/advantage_air/translations/id.json b/homeassistant/components/advantage_air/translations/id.json new file mode 100644 index 00000000000..7993fa3be1d --- /dev/null +++ b/homeassistant/components/advantage_air/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "port": "Port" + }, + "description": "Hubungkan ke API tablet dinding Advantage Air.", + "title": "Hubungkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/ko.json b/homeassistant/components/advantage_air/translations/ko.json index 444d8d38285..9a28cc499bc 100644 --- a/homeassistant/components/advantage_air/translations/ko.json +++ b/homeassistant/components/advantage_air/translations/ko.json @@ -11,7 +11,9 @@ "data": { "ip_address": "IP \uc8fc\uc18c", "port": "\ud3ec\ud2b8" - } + }, + "description": "\ubcbd\uc5d0 \ubd80\ucc29\ub41c Advantage Air \ud0dc\ube14\ub9bf\uc758 API\uc5d0 \uc5f0\uacb0\ud558\uae30", + "title": "\uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 58b1a3b10f0..4c1315d187d 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -8,18 +8,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from .const import COMPONENTS, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR +from .const import DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, PLATFORMS from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the AEMET OpenData component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up AEMET OpenData as config entry.""" name = config_entry.data[CONF_NAME] @@ -30,16 +24,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): aemet = AEMET(api_key) weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude) - await weather_coordinator.async_refresh() + await weather_coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = { ENTRY_NAME: name, ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for component in COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -50,8 +45,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in COMPONENTS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/aemet/abstract_aemet_sensor.py b/homeassistant/components/aemet/abstract_aemet_sensor.py index 6b7c3c69fee..8847a5d094d 100644 --- a/homeassistant/components/aemet/abstract_aemet_sensor.py +++ b/homeassistant/components/aemet/abstract_aemet_sensor.py @@ -1,4 +1,5 @@ """Abstraction form AEMET OpenData sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -6,7 +7,7 @@ from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT from .weather_update_coordinator import WeatherUpdateCoordinator -class AbstractAemetSensor(CoordinatorEntity): +class AbstractAemetSensor(CoordinatorEntity, SensorEntity): """Abstract class for an AEMET OpenData sensor.""" def __init__( @@ -52,6 +53,6 @@ class AbstractAemetSensor(CoordinatorEntity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 27f389660a8..2e36896c1eb 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -6,8 +6,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_NAME -from .const import DOMAIN # pylint:disable=unused-import +from .const import DEFAULT_NAME, DOMAIN class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 13b9d944bf0..390ccb86003 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -34,7 +34,7 @@ from homeassistant.const import ( ) ATTRIBUTION = "Powered by AEMET OpenData" -COMPONENTS = ["sensor", "weather"] +PLATFORMS = ["sensor", "weather"] DEFAULT_NAME = "AEMET" DOMAIN = "aemet" ENTRY_NAME = "name" diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index b57de1ce890..6f43d66e011 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -106,9 +106,10 @@ class AemetForecastSensor(AbstractAemetSensor): @property def state(self): """Return the state of the device.""" + forecast = None forecasts = self._weather_coordinator.data.get( FORECAST_MODE_ATTR_API[self._forecast_mode] ) if forecasts: - return forecasts[0].get(self._sensor_type) - return None + forecast = forecasts[0].get(self._sensor_type) + return forecast diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json index d7254aea92f..d5312805722 100644 --- a/homeassistant/components/aemet/translations/de.json +++ b/homeassistant/components/aemet/translations/de.json @@ -11,8 +11,11 @@ "data": { "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad" - } + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "[void]" } } } diff --git a/homeassistant/components/aemet/translations/hu.json b/homeassistant/components/aemet/translations/hu.json new file mode 100644 index 00000000000..d810691046e --- /dev/null +++ b/homeassistant/components/aemet/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "Az integr\u00e1ci\u00f3 neve" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/id.json b/homeassistant/components/aemet/translations/id.json new file mode 100644 index 00000000000..fa678cbbbe0 --- /dev/null +++ b/homeassistant/components/aemet/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama integrasi" + }, + "description": "Siapkan integrasi AEMET OpenData. Untuk menghasilkan kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario", + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/ko.json b/homeassistant/components/aemet/translations/ko.json index edfb023a88b..95c11b018fe 100644 --- a/homeassistant/components/aemet/translations/ko.json +++ b/homeassistant/components/aemet/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -14,6 +14,7 @@ "longitude": "\uacbd\ub3c4", "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" }, + "description": "AEMET OpenData \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://opendata.aemet.es/centrodedescargas/altaUsuario \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/lb.json b/homeassistant/components/aemet/translations/lb.json new file mode 100644 index 00000000000..8e83c0e86d3 --- /dev/null +++ b/homeassistant/components/aemet/translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "L\u00e4ngegrad", + "longitude": "Breedegrad", + "name": "Numm vun der Integratioun" + }, + "title": "AEMET OpenData" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json index 02415dde1e6..77589e20490 100644 --- a/homeassistant/components/aemet/translations/nl.json +++ b/homeassistant/components/aemet/translations/nl.json @@ -14,6 +14,7 @@ "longitude": "Lengtegraad", "name": "Naam van de integratie" }, + "description": "Stel AEMET OpenData-integratie in. Ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario om een API-sleutel te genereren", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 619429c9a5b..a7ca0a12422 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -82,6 +82,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +STATION_MAX_DELTA = timedelta(hours=2) WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) @@ -90,7 +91,7 @@ def format_condition(condition: str) -> str: for key, value in CONDITIONS_MAP.items(): if condition in value: return key - _LOGGER.error('condition "%s" not found in CONDITIONS_MAP', condition) + _LOGGER.error('Condition "%s" not found in CONDITIONS_MAP', condition) return condition @@ -98,7 +99,7 @@ def format_float(value) -> float: """Try converting string to float.""" try: return float(value) - except ValueError: + except (TypeError, ValueError): return None @@ -106,7 +107,7 @@ def format_int(value) -> int: """Try converting string to int.""" try: return int(value) - except ValueError: + except (TypeError, ValueError): return None @@ -175,14 +176,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ) if self._town: _LOGGER.debug( - "town found for coordinates [%s, %s]: %s", + "Town found for coordinates [%s, %s]: %s", self._latitude, self._longitude, self._town, ) if not self._town: _LOGGER.error( - "town not found for coordinates [%s, %s]", + "Town not found for coordinates [%s, %s]", self._latitude, self._longitude, ) @@ -197,7 +198,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) if not daily: _LOGGER.error( - 'error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] + 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] ) hourly = self._aemet.get_specific_forecast_town_hourly( @@ -205,7 +206,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ) if not hourly: _LOGGER.error( - 'error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID] + 'Error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID] ) station = None @@ -215,7 +216,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ) if not station: _LOGGER.error( - 'error fetching data for station "%s"', + 'Error fetching data for station "%s"', self._station[AEMET_ATTR_IDEMA], ) @@ -241,6 +242,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): weather_response.hourly[ATTR_DATA][0][AEMET_ATTR_ELABORATED] ) now = dt_util.now() + now_utc = dt_util.utcnow() hour = now.hour # Get current day @@ -253,10 +255,18 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): day = cur_day break - # Get station data + # Get latest station data station_data = None + station_dt = None if weather_response.station: - station_data = weather_response.station[ATTR_DATA][-1] + for _station_data in weather_response.station[ATTR_DATA]: + if AEMET_ATTR_STATION_DATE in _station_data: + _station_dt = dt_util.parse_datetime( + _station_data[AEMET_ATTR_STATION_DATE] + "Z" + ) + if not station_dt or _station_dt > station_dt: + station_data = _station_data + station_dt = _station_dt condition = None humidity = None @@ -273,7 +283,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_feeling = None town_id = None town_name = None - town_timestamp = dt_util.as_utc(elaborated) + town_timestamp = dt_util.as_utc(elaborated).isoformat() wind_bearing = None wind_max_speed = None wind_speed = None @@ -299,17 +309,20 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): # Overwrite weather values with closest station data (if present) if station_data: - if AEMET_ATTR_STATION_DATE in station_data: - station_dt = dt_util.parse_datetime( - station_data[AEMET_ATTR_STATION_DATE] + "Z" - ) - station_timestamp = dt_util.as_utc(station_dt).isoformat() - if AEMET_ATTR_STATION_HUMIDITY in station_data: - humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY]) - if AEMET_ATTR_STATION_PRESSURE_SEA in station_data: - pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE_SEA]) - if AEMET_ATTR_STATION_TEMPERATURE in station_data: - temperature = format_float(station_data[AEMET_ATTR_STATION_TEMPERATURE]) + station_timestamp = dt_util.as_utc(station_dt).isoformat() + if (now_utc - station_dt) <= STATION_MAX_DELTA: + if AEMET_ATTR_STATION_HUMIDITY in station_data: + humidity = format_float(station_data[AEMET_ATTR_STATION_HUMIDITY]) + if AEMET_ATTR_STATION_PRESSURE_SEA in station_data: + pressure = format_float( + station_data[AEMET_ATTR_STATION_PRESSURE_SEA] + ) + if AEMET_ATTR_STATION_TEMPERATURE in station_data: + temperature = format_float( + station_data[AEMET_ATTR_STATION_TEMPERATURE] + ) + else: + _LOGGER.warning("Station data is outdated") # Get forecast from weather data forecast_daily = self._get_daily_forecast_from_weather_response( @@ -535,9 +548,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_temperature(day_data, hour): """Get temperature (hour) from weather data.""" val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE], hour) - if val: - return format_int(val) - return None + return format_int(val) @staticmethod def _get_temperature_day(day_data): @@ -545,9 +556,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): val = get_forecast_day_value( day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MAX ) - if val: - return format_int(val) - return None + return format_int(val) @staticmethod def _get_temperature_low_day(day_data): @@ -555,17 +564,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): val = get_forecast_day_value( day_data[AEMET_ATTR_TEMPERATURE], key=AEMET_ATTR_MIN ) - if val: - return format_int(val) - return None + return format_int(val) @staticmethod def _get_temperature_feeling(day_data, hour): """Get temperature from weather data.""" val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour) - if val: - return format_int(val) - return None + return format_int(val) def _get_town_id(self): """Get town ID from weather data.""" diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 2d9021f8009..a5ffc511a26 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -5,12 +5,11 @@ import logging from pyaftership.tracker import Tracking import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from .const import DOMAIN @@ -108,7 +107,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class AfterShipSensor(Entity): +class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" def __init__(self, aftership, name): @@ -134,7 +133,7 @@ class AfterShipSensor(Entity): return "packages" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return attributes for the sensor.""" return self._attributes diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index fc52ee0ef36..3623f4f702a 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -16,11 +16,6 @@ DEFAULT_BRAND = "Agent DVR by ispyconnect.com" FORWARDS = ["alarm_control_panel", "camera"] -async def async_setup(hass, config): - """Old way to set up integrations.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up the Agent component.""" hass.data.setdefault(AGENT_DOMAIN, {}) diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 571b5239de7..24cd5dbb92c 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -102,13 +102,13 @@ class AgentCamera(MjpegCamera): _LOGGER.debug("%s reacquired", self._name) self._removed = False except AgentError: - if self.device.client.is_available: # server still available - camera error - if not self._removed: - _LOGGER.error("%s lost", self._name) - self._removed = True + # server still available - camera error + if self.device.client.is_available and not self._removed: + _LOGGER.error("%s lost", self._name) + self._removed = True @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the Agent DVR camera state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py index 15ef58ced7e..a21e6855337 100644 --- a/homeassistant/components/agent_dvr/config_flow.py +++ b/homeassistant/components/agent_dvr/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, SERVER_URL # pylint:disable=unused-import +from .const import DOMAIN, SERVER_URL from .helpers import generate_url DEFAULT_PORT = 8090 diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index 1d28556ba1a..49968ceea75 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/agent_dvr/translations/id.json b/homeassistant/components/agent_dvr/translations/id.json new file mode 100644 index 00000000000..f2ee1cc6622 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Siapkan Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 52c9208854a..d69a02f83bd 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -1,6 +1,7 @@ """Component for handling Air Quality data for your location.""" from datetime import timedelta import logging +from typing import final from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -131,6 +132,7 @@ class AirQualityEntity(Entity): """Return the NO2 (nitrogen dioxide) level.""" return None + @final @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/air_quality/group.py b/homeassistant/components/air_quality/group.py index 4741f8a3b54..2ac081496cd 100644 --- a/homeassistant/components/air_quality/group.py +++ b/homeassistant/components/air_quality/group.py @@ -2,13 +2,12 @@ from homeassistant.components.group import GroupIntegrationRegistry -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.exclude_domain() diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 6a9c23624f0..41a7c03e636 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -10,8 +10,6 @@ from airly.exceptions import AirlyError import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -44,11 +42,6 @@ def set_update_interval(hass, instances): return interval -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured Airly.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up Airly as config entry.""" api_key = config_entry.data[CONF_API_KEY] @@ -71,17 +64,14 @@ async def async_setup_entry(hass, config_entry): coordinator = AirlyDataUpdateCoordinator( hass, websession, api_key, latitude, longitude, update_interval, use_nearest ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -92,8 +82,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -144,6 +134,12 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): except (AirlyError, ClientConnectorError) as error: raise UpdateFailed(error) from error + _LOGGER.debug( + "Requests remaining: %s/%s", + self.airly.requests_remaining, + self.airly.requests_per_day, + ) + values = measurements.current["values"] index = measurements.current["indexes"][0] standards = measurements.current["standards"] diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 4c4c239a84b..f89a804ab3b 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -117,7 +117,7 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = { LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index d7636d1db33..3b1432f77ba 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -16,11 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import ( # pylint:disable=unused-import - CONF_USE_NEAREST, - DOMAIN, - NO_AIRLY_SENSORS, -) +from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json index 77de843ffce..a5ff485d1d0 100644 --- a/homeassistant/components/airly/manifest.json +++ b/homeassistant/components/airly/manifest.json @@ -3,7 +3,7 @@ "name": "Airly", "documentation": "https://www.home-assistant.io/integrations/airly", "codeowners": ["@bieniu"], - "requirements": ["airly==1.0.0"], + "requirements": ["airly==1.1.0"], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 789dbbb4657..2a52050db15 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,4 +1,5 @@ """Support for the Airly sensor service.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -72,7 +73,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class AirlySensor(CoordinatorEntity): +class AirlySensor(CoordinatorEntity, SensorEntity): """Define an Airly sensor.""" def __init__(self, coordinator, name, kind): @@ -102,7 +103,7 @@ class AirlySensor(CoordinatorEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index afda73ae887..c6b6f1e6a41 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Reach Airly server" + "can_reach_server": "Reach Airly server", + "requests_remaining": "Remaining allowed requests", + "requests_per_day": "Allowed requests per day" } } } diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py index 6b683518ebd..3f2ed8e8d65 100644 --- a/homeassistant/components/airly/system_health.py +++ b/homeassistant/components/airly/system_health.py @@ -4,6 +4,8 @@ from airly import Airly from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback +from .const import DOMAIN + @callback def async_register( @@ -15,8 +17,13 @@ def async_register( async def system_health_info(hass): """Get info for the info page.""" + requests_remaining = list(hass.data[DOMAIN].values())[0].airly.requests_remaining + requests_per_day = list(hass.data[DOMAIN].values())[0].airly.requests_per_day + return { "can_reach_server": system_health.async_check_can_reach_url( hass, Airly.AIRLY_API_URL - ) + ), + "requests_remaining": requests_remaining, + "requests_per_day": requests_per_day, } diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index 8004444fdb9..b13798c0ae3 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Airly-Server erreichen" + "can_reach_server": "Airly-Server erreichen", + "requests_per_day": "Erlaubte Anfragen pro Tag", + "requests_remaining": "Verbleibende erlaubte Anfragen" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index a0ed36a7169..a96a2f62293 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Alcanzar el servidor Airly" + "can_reach_server": "Alcanzar el servidor Airly", + "requests_per_day": "Solicitudes permitidas por d\u00eda", + "requests_remaining": "Solicitudes permitidas restantes" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index c5c9359c67f..6730e131ac2 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -23,8 +23,8 @@ "system_health": { "info": { "can_reach_server": "\u00dchendus Airly serveriga", - "requests_per_day": "Lubatud taotlusi p\u00e4evas", - "requests_remaining": "J\u00e4\u00e4nud lubatud taotlusi" + "requests_per_day": "Lubatud p\u00e4ringuid p\u00e4evas", + "requests_remaining": "Lubatud p\u00e4ringute arv" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 98407155f17..a23f455e0b8 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Acc\u00e8s au serveur Airly" + "can_reach_server": "Acc\u00e8s au serveur Airly", + "requests_per_day": "Demandes autoris\u00e9es par jour", + "requests_remaining": "Demandes autoris\u00e9es restantes" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/he.json b/homeassistant/components/airly/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/airly/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index b96734935a0..b9fd0c9e05c 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van." + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" }, "error": { + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." }, "step": { @@ -12,11 +13,17 @@ "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "Az integr\u00e1ci\u00f3 neve" + "name": "N\u00e9v" }, "description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register", "title": "Airly" } } + }, + "system_health": { + "info": { + "requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta", + "requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/id.json b/homeassistant/components/airly/translations/id.json new file mode 100644 index 00000000000..57b4c0d95f9 --- /dev/null +++ b/homeassistant/components/airly/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "invalid_api_key": "Kunci API tidak valid", + "wrong_location": "Tidak ada stasiun pengukur Airly di daerah ini." + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Siapkan integrasi kualitas udara Airly. Untuk membuat kunci API, buka https://developer.airly.eu/register", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server Airly", + "requests_per_day": "Permintaan yang diizinkan per hari", + "requests_remaining": "Sisa permintaan yang diizinkan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index bf6d7a461ce..385b8117437 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Raggiungi il server Airly" + "can_reach_server": "Raggiungi il server Airly", + "requests_per_day": "Richieste consentite al giorno", + "requests_remaining": "Richieste consentite rimanenti" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/ko.json b/homeassistant/components/airly/translations/ko.json index 95981ea5eb1..1f9db4a592e 100644 --- a/homeassistant/components/airly/translations/ko.json +++ b/homeassistant/components/airly/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -19,5 +19,12 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Airly \uc11c\ubc84 \uc5f0\uacb0", + "requests_per_day": "\uc77c\uc77c \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218", + "requests_remaining": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json index a7bc9966f63..14cbaf1711e 100644 --- a/homeassistant/components/airly/translations/nl.json +++ b/homeassistant/components/airly/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Airly-integratie voor deze co\u00f6rdinaten is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd." }, "error": { "invalid_api_key": "Ongeldige API-sleutel", @@ -10,14 +10,21 @@ "step": { "user": { "data": { - "api_key": "Airly API-sleutel", + "api_key": "API-sleutel", "latitude": "Breedtegraad", "longitude": "Lengtegraad", - "name": "Naam van de integratie" + "name": "Naam" }, "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren", "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Kan Airly server bereiken", + "requests_per_day": "Toegestane verzoeken per dag", + "requests_remaining": "Resterende toegestane verzoeken" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index b38568210ad..4c81422d93c 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "N\u00e5 Airly-serveren" + "can_reach_server": "N\u00e5 Airly-serveren", + "requests_per_day": "Tillatte foresp\u00f8rsler per dag", + "requests_remaining": "Gjenv\u00e6rende tillatte foresp\u00f8rsler" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index e36e6f86ec7..f205a569474 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -22,7 +22,9 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera Airly" + "can_reach_server": "Dost\u0119p do serwera Airly", + "requests_per_day": "Dozwolone dzienne \u017c\u0105dania", + "requests_remaining": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/zh-Hans.json b/homeassistant/components/airly/translations/zh-Hans.json index 1a57bfbadf9..0f3c5137d31 100644 --- a/homeassistant/components/airly/translations/zh-Hans.json +++ b/homeassistant/components/airly/translations/zh-Hans.json @@ -13,7 +13,8 @@ }, "system_health": { "info": { - "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668" + "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668", + "requests_per_day": "\u5141\u8bb8\u6bcf\u5929\u8bf7\u6c42" } } } \ No newline at end of file diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 5cbc87947f9..b1770dcbde7 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -11,7 +11,6 @@ from pyairnow.errors import AirNowError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS 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, UpdateFailed @@ -38,11 +37,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the AirNow component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up AirNow from a config entry.""" api_key = entry.data[CONF_API_KEY] @@ -60,17 +54,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Sync with Coordinator - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() # Store Entity and Initialize Platforms hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -81,8 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py index 6d53ac133ee..b4de58808da 100644 --- a/homeassistant/components/airnow/config_flow.py +++ b/homeassistant/components/airnow/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 4488098701f..2d3adc8d1e2 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -1,4 +1,5 @@ """Support for the AirNow sensor service.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, @@ -59,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class AirNowSensor(CoordinatorEntity): +class AirNowSensor(CoordinatorEntity, SensorEntity): """Define an AirNow sensor.""" def __init__(self, coordinator, kind): @@ -84,7 +85,7 @@ class AirNowSensor(CoordinatorEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.kind == ATTR_API_AQI: self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[ diff --git a/homeassistant/components/airnow/translations/bg.json b/homeassistant/components/airnow/translations/bg.json new file mode 100644 index 00000000000..5d274ec2b73 --- /dev/null +++ b/homeassistant/components/airnow/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json index c98fc6d7415..8c2b47c1bd4 100644 --- a/homeassistant/components/airnow/translations/de.json +++ b/homeassistant/components/airnow/translations/de.json @@ -16,6 +16,7 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, + "description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/hu.json b/homeassistant/components/airnow/translations/hu.json new file mode 100644 index 00000000000..418450f2419 --- /dev/null +++ b/homeassistant/components/airnow/translations/hu.json @@ -0,0 +1,24 @@ +{ + "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", + "invalid_location": "Erre a helyre nem tal\u00e1lhat\u00f3 eredm\u00e9ny", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/id.json b/homeassistant/components/airnow/translations/id.json new file mode 100644 index 00000000000..66fdff72fae --- /dev/null +++ b/homeassistant/components/airnow/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_location": "Tidak ada hasil yang ditemukan untuk lokasi tersebut", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "radius": "Radius Stasiun (mil; opsional)" + }, + "description": "Siapkan integrasi kualitas udara AirNow. Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ko.json b/homeassistant/components/airnow/translations/ko.json index 6da62dffa2c..adfbf0be8ed 100644 --- a/homeassistant/components/airnow/translations/ko.json +++ b/homeassistant/components/airnow/translations/ko.json @@ -6,6 +6,7 @@ "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_location": "\ud574\ub2f9 \uc704\uce58\uc5d0 \uacb0\uacfc\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { @@ -13,9 +14,13 @@ "data": { "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4" - } + "longitude": "\uacbd\ub3c4", + "radius": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158 \ubc18\uacbd (\ub9c8\uc77c; \uc120\ud0dd \uc0ac\ud56d)" + }, + "description": "AirNow \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://docs.airnowapi.org/account/request \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "AirNow" } } - } + }, + "title": "AirNow" } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/nl.json b/homeassistant/components/airnow/translations/nl.json index 011498269f8..090a5363823 100644 --- a/homeassistant/components/airnow/translations/nl.json +++ b/homeassistant/components/airnow/translations/nl.json @@ -14,8 +14,10 @@ "data": { "api_key": "API-sleutel", "latitude": "Breedtegraad", - "longitude": "Lengtegraad" + "longitude": "Lengtegraad", + "radius": "Stationsradius (mijl; optioneel)" }, + "description": "AirNow luchtkwaliteit integratie opzetten. Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f82bdcd96b8..f02020d25b4 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -23,7 +23,6 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -80,9 +79,9 @@ def async_get_cloud_api_update_interval(hass, api_key, num_consumers): This will shift based on the number of active consumers, thus keeping the user under the monthly API limit. """ - # Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note + # Assuming 10,000 calls per month and a "largest possible month" of 31 days; note # that we give a buffer of 1500 API calls for any drift, restarts, etc.: - minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers)) + minutes_between_api_calls = ceil(num_consumers * 31 * 24 * 60 / 8500) LOGGER.debug( "Leveling API key usage (%s): %s consumers, %s minutes between updates", @@ -243,10 +242,6 @@ async def async_setup_entry(hass, config_entry): update_method=async_update_data, ) - async_sync_geo_coordinator_update_intervals( - hass, config_entry.data[CONF_API_KEY] - ) - # Only geography-based entries have options: hass.data[DOMAIN][DATA_LISTENER][ config_entry.entry_id @@ -272,15 +267,19 @@ async def async_setup_entry(hass, config_entry): update_method=async_update_data, ) - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator - for component in PLATFORMS: + # Reassess the interval between 2 server requests + if CONF_API_KEY in config_entry.data: + async_sync_geo_coordinator_update_intervals( + hass, config_entry.data[CONF_API_KEY] + ) + + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -333,8 +332,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -343,10 +342,7 @@ async def async_unload_entry(hass, config_entry): remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) remove_listener() - if ( - config_entry.data[CONF_INTEGRATION_TYPE] - == INTEGRATION_TYPE_GEOGRAPHY_COORDS - ): + if CONF_API_KEY in config_entry.data: # Re-calculate the update interval period for any remaining consumers of # this API key: async_sync_geo_coordinator_update_intervals( @@ -372,7 +368,7 @@ class AirVisualEntity(CoordinatorEntity): self._unit = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 12dec114349..128a06b7400 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv from . import async_get_geography_id -from .const import ( # pylint: disable=unused-import +from .const import ( CONF_CITY, CONF_COUNTRY, CONF_INTEGRATION_TYPE, diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 3c1aef128ab..1febcec68f4 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,4 +1,5 @@ """Support for AirVisual air quality sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -138,7 +139,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, True) -class AirVisualGeographySensor(AirVisualEntity): +class AirVisualGeographySensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to geography data via the Cloud API.""" def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale): @@ -236,7 +237,7 @@ class AirVisualGeographySensor(AirVisualEntity): self._attrs.pop(ATTR_LONGITUDE, None) -class AirVisualNodeProSensor(AirVisualEntity): +class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" def __init__(self, coordinator, kind, name, device_class, unit): diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json new file mode 100644 index 00000000000..7e463418576 --- /dev/null +++ b/homeassistant/components/airvisual/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "geography_by_name": { + "data": { + "city": "\u0413\u0440\u0430\u0434", + "country": "\u0421\u0442\u0440\u0430\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index 7d1a1c696ed..e32efda96dc 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -6,7 +6,13 @@ "step": { "geography": { "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + }, + "node_pro": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } } } diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 53ab734e505..89584cd128b 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van vagy a Node/Pro azonos\u00edt\u00f3 m\u00e1r regisztr\u00e1lva van.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt." + "general_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" }, "step": { "geography": { @@ -12,8 +17,23 @@ "longitude": "Hossz\u00fas\u00e1g" } }, + "geography_by_coords": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + }, + "geography_by_name": { + "data": { + "api_key": "API kulcs", + "city": "V\u00e1ros", + "country": "Orsz\u00e1g" + } + }, "node_pro": { "data": { + "ip_address": "Hoszt", "password": "Jelsz\u00f3" } }, @@ -21,6 +41,11 @@ "data": { "api_key": "API kulcs" } + }, + "user": { + "data": { + "type": "Integr\u00e1ci\u00f3 t\u00edpusa" + } } } } diff --git a/homeassistant/components/airvisual/translations/id.json b/homeassistant/components/airvisual/translations/id.json new file mode 100644 index 00000000000..3c689338d9f --- /dev/null +++ b/homeassistant/components/airvisual/translations/id.json @@ -0,0 +1,77 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi atau ID Node/Pro sudah terdaftar.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "general_error": "Kesalahan yang tidak diharapkan", + "invalid_api_key": "Kunci API tidak valid", + "location_not_found": "Lokasi tidak ditemukan" + }, + "step": { + "geography": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur" + }, + "description": "Gunakan API cloud AirVisual untuk memantau lokasi geografis.", + "title": "Konfigurasikan Lokasi Geografi" + }, + "geography_by_coords": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur" + }, + "description": "Gunakan API cloud AirVisual untuk memantau satu pasang lintang/bujur.", + "title": "Konfigurasikan Lokasi Geografi" + }, + "geography_by_name": { + "data": { + "api_key": "Kunci API", + "city": "Kota", + "country": "Negara", + "state": "negara bagian" + }, + "description": "Gunakan API cloud AirVisual untuk memantau kota/negara bagian/negara.", + "title": "Konfigurasikan Lokasi Geografi" + }, + "node_pro": { + "data": { + "ip_address": "Host", + "password": "Kata Sandi" + }, + "description": "Pantau unit AirVisual pribadi. Kata sandi dapat diambil dari antarmuka unit.", + "title": "Konfigurasikan AirVisual Node/Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "title": "Autentikasi Ulang AirVisual" + }, + "user": { + "data": { + "cloud_api": "Lokasi Geografis", + "node_pro": "AirVisual Node Pro", + "type": "Jenis Integrasi" + }, + "description": "Pilih jenis data AirVisual yang ingin dipantau.", + "title": "Konfigurasikan AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Tampilkan lokasi geografi yang dipantau pada peta" + }, + "title": "Konfigurasikan AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index 8cf450e597b..3ab3dbcf286 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "already_configured": "Node/Pro ID \uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured": "Node/Pro ID\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uac70\ub098 \uc704\uce58\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", "general_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "location_not_found": "\uc704\uce58\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { "geography": { @@ -24,12 +25,19 @@ "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4" - } + }, + "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc704\ub3c4/\uacbd\ub3c4\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" }, "geography_by_name": { "data": { - "api_key": "API \ud0a4" - } + "api_key": "API \ud0a4", + "city": "\ub3c4\uc2dc", + "country": "\uad6d\uac00", + "state": "\uc8fc" + }, + "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub3c4\uc2dc/\uc8fc/\uad6d\uac00\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" }, "node_pro": { "data": { @@ -42,7 +50,8 @@ "reauth_confirm": { "data": { "api_key": "API \ud0a4" - } + }, + "title": "AirVisual \uc7ac\uc778\uc99d\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index ecf2322c801..ed81d6568ed 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd.", + "already_configured": "Locatie is al geconfigureerd. of Node/Pro IDis al geregistreerd.", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { @@ -25,15 +25,19 @@ "api_key": "API-sleutel", "latitude": "Breedtegraad", "longitude": "Lengtegraad" - } + }, + "description": "Gebruik de AirVisual-cloud-API om een lengte- / breedtegraad te bewaken.", + "title": "Configureer een geografie" }, "geography_by_name": { "data": { "api_key": "API-sleutel", "city": "Stad", - "country": "Land" + "country": "Land", + "state": "staat" }, - "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken." + "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.", + "title": "Configureer een geografie" }, "node_pro": { "data": { diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 114abfa9cd6..7d9e47fbcbe 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -2,6 +2,7 @@ from abc import abstractmethod from datetime import timedelta import logging +from typing import final import voluptuous as vol @@ -172,6 +173,7 @@ class AlarmControlPanelEntity(Entity): def supported_features(self) -> int: """Return the list of supported features.""" + @final @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index 0dc16fdcf42..9a55998e929 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Alarm control panel.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -41,7 +41,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Alarm control panel devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -109,11 +109,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if CONF_CODE in config: service_data[ATTR_CODE] = config[CONF_CODE] diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index e5b3ec6aeee..3817cf37b45 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -1,5 +1,5 @@ """Provide the device automations for Alarm control panel.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -58,7 +58,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Alarm control panel devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 5669340c2ce..b24716bb43e 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Alarm control panel.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, + CONF_FOR, CONF_PLATFORM, CONF_TYPE, STATE_ALARM_ARMED_AWAY, @@ -31,24 +32,19 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_TYPES = { - "triggered", - "disarmed", - "arming", - "armed_home", - "armed_away", - "armed_night", -} +BASIC_TRIGGER_TYPES = {"triggered", "disarmed", "arming"} +TRIGGER_TYPES = BASIC_TRIGGER_TYPES | {"armed_home", "armed_away", "armed_night"} TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Alarm control panel devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -67,56 +63,38 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES] # Add triggers for each entity that belongs to this integration + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + triggers += [ { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "disarmed", - }, - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "triggered", - }, - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arming", - }, + **base_trigger, + CONF_TYPE: trigger, + } + for trigger in BASIC_TRIGGER_TYPES ] if supported_features & SUPPORT_ALARM_ARM_HOME: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "armed_home", } ) if supported_features & SUPPORT_ALARM_ARM_AWAY: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "armed_away", } ) if supported_features & SUPPORT_ALARM_ARM_NIGHT: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "armed_night", } ) @@ -124,6 +102,15 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return triggers +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -131,8 +118,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] == "triggered": to_state = STATE_ALARM_TRIGGERED elif config[CONF_TYPE] == "disarmed": @@ -151,6 +136,8 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_trigger.CONF_TO: to_state, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] state_config = state_trigger.TRIGGER_SCHEMA(state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py index 6645f12245d..4bfb1486814 100644 --- a/homeassistant/components/alarm_control_panel/group.py +++ b/homeassistant/components/alarm_control_panel/group.py @@ -10,13 +10,12 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_OFF, ) -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/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index 9e7d8e6f1a7..90979d97dd0 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Alarm control panel state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -18,8 +20,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -36,11 +37,11 @@ VALID_STATES = { async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -80,11 +81,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Alarm control panel states.""" await asyncio.gather( diff --git a/homeassistant/components/alarm_control_panel/translations/id.json b/homeassistant/components/alarm_control_panel/translations/id.json index cbc3d31370c..f1676ce8c75 100644 --- a/homeassistant/components/alarm_control_panel/translations/id.json +++ b/homeassistant/components/alarm_control_panel/translations/id.json @@ -1,14 +1,37 @@ { + "device_automation": { + "action_type": { + "arm_away": "Aktifkan {entity_name} untuk keluar", + "arm_home": "Aktifkan {entity_name} untuk di rumah", + "arm_night": "Aktifkan {entity_name} untuk malam", + "disarm": "Nonaktifkan {entity_name}", + "trigger": "Picu {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} diaktifkan untuk keluar", + "is_armed_home": "{entity_name} diaktifkan untuk di rumah", + "is_armed_night": "{entity_name} diaktifkan untuk malam", + "is_disarmed": "{entity_name} dinonaktifkan", + "is_triggered": "{entity_name} dipicu" + }, + "trigger_type": { + "armed_away": "{entity_name} diaktifkan untuk keluar", + "armed_home": "{entity_name} diaktifkan untuk di rumah", + "armed_night": "{entity_name} diaktifkan untuk malam", + "disarmed": "{entity_name} dinonaktifkan", + "triggered": "{entity_name} dipicu" + } + }, "state": { "_": { - "armed": "Bersenjata", - "armed_away": "Armed away", - "armed_custom_bypass": "Armed custom bypass", - "armed_home": "Armed home", - "armed_night": "Armed night", - "arming": "Mempersenjatai", - "disarmed": "Dilucuti", - "disarming": "Melucuti", + "armed": "Diaktifkan", + "armed_away": "Diaktifkan untuk keluar", + "armed_custom_bypass": "Diaktifkan khusus", + "armed_home": "Diaktifkan untuk di rumah", + "armed_night": "Diaktifkan untuk malam", + "arming": "Mengaktifkan", + "disarmed": "Dinonaktifkan", + "disarming": "Dinonaktifkan", "pending": "Tertunda", "triggered": "Terpicu" } diff --git a/homeassistant/components/alarm_control_panel/translations/ko.json b/homeassistant/components/alarm_control_panel/translations/ko.json index f6adb68fe66..0fd766ba0b9 100644 --- a/homeassistant/components/alarm_control_panel/translations/ko.json +++ b/homeassistant/components/alarm_control_panel/translations/ko.json @@ -1,35 +1,35 @@ { "device_automation": { "action_type": { - "arm_away": "{entity_name} \uc678\ucd9c\uacbd\ube44", - "arm_home": "{entity_name} \uc7ac\uc2e4\uacbd\ube44", - "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", - "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", - "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + "arm_away": "{entity_name}\uc744(\ub97c) \uc678\ucd9c\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30", + "arm_home": "{entity_name}\uc744(\ub97c) \uc7ac\uc2e4\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30", + "arm_night": "{entity_name}\uc744(\ub97c) \uc57c\uac04\uacbd\ube44\ub85c \uc124\uc815\ud558\uae30", + "disarm": "{entity_name}\uc744(\ub97c) \uacbd\ube44\ud574\uc81c\ub85c \uc124\uc815\ud558\uae30", + "trigger": "{entity_name}\uc744(\ub97c) \ud2b8\ub9ac\uac70\ud558\uae30" }, "condition_type": { - "is_armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", - "is_armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", - "is_armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", - "is_disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c \uc0c1\ud0dc\uc774\uba74", - "is_triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub418\uc5c8\uc73c\uba74" + "is_armed_away": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_armed_home": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_armed_night": "{entity_name}\uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_disarmed": "{entity_name}\uc774(\uac00) \ud574\uc81c \uc0c1\ud0dc\uc774\uba74", + "is_triggered": "{entity_name}\uc774(\uac00) \ud2b8\ub9ac\uac70 \ub418\uc5c8\uc73c\uba74" }, "trigger_type": { - "armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", - "armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", - "armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", - "disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c\ub420 \ub54c", - "triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub420 \ub54c" + "armed_away": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "armed_home": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "armed_night": "{entity_name}\uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "disarmed": "{entity_name}\uc774(\uac00) \ud574\uc81c\ub418\uc5c8\uc744 \ub54c", + "triggered": "{entity_name}\uc774(\uac00) \ud2b8\ub9ac\uac70\ub418\uc5c8\uc744 \ub54c" } }, "state": { "_": { - "armed": "\uacbd\ube44\uc911", - "armed_away": "\uacbd\ube44\uc911(\uc678\ucd9c)", - "armed_custom_bypass": "\uacbd\ube44\uc911(\uc0ac\uc6a9\uc790 \uc6b0\ud68c)", - "armed_home": "\uacbd\ube44\uc911(\uc7ac\uc2e4)", - "armed_night": "\uacbd\ube44\uc911(\uc57c\uac04)", - "arming": "\uacbd\ube44\uc911", + "armed": "\uacbd\ube44 \uc911", + "armed_away": "\uacbd\ube44 \uc911(\uc678\ucd9c)", + "armed_custom_bypass": "\uacbd\ube44 \uc911 (\uc0ac\uc6a9\uc790 \uc6b0\ud68c)", + "armed_home": "\uacbd\ube44 \uc911(\uc7ac\uc2e4)", + "armed_night": "\uacbd\ube44 \uc911(\uc57c\uac04)", + "arming": "\uacbd\ube44 \uc911", "disarmed": "\ud574\uc81c\ub428", "disarming": "\ud574\uc81c\uc911", "pending": "\ubcf4\ub958\uc911", diff --git a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json index 749674e8e6e..fa819e71b49 100644 --- a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json +++ b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json @@ -1,4 +1,27 @@ { + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", + "arm_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", + "arm_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "disarm": "\u89e3\u9664 {entity_name} \u8b66\u6212", + "trigger": "\u89e6\u53d1 {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", + "is_armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", + "is_armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "is_disarmed": "{entity_name} \u8b66\u6212\u5df2\u89e3\u9664", + "is_triggered": "{entity_name} \u8b66\u62a5\u5df2\u89e6\u53d1" + }, + "trigger_type": { + "armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", + "armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", + "armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "disarmed": "{entity_name} \u8b66\u6212\u89e3\u9664", + "triggered": "{entity_name} \u89e6\u53d1\u8b66\u62a5" + } + }, "state": { "_": { "armed": "\u8b66\u6212", diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 8dd704f1333..849aae9b3cc 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -39,11 +39,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"] -async def async_setup(hass, config): - """Set up for the AlarmDecoder devices.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up AlarmDecoder config flow.""" undo_listener = entry.add_update_listener(_update_listener) @@ -130,9 +125,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await open_connection() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -144,8 +139,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 5e3118b1d3d..9cab2afa43c 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -163,7 +163,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): return self._code_arm_required @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { "ac_power": self._ac_power, diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 55bf13d7fef..4cc3bb6b5cf 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -118,7 +118,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attr = {CONF_ZONE_NUMBER: self._zone_number} if self._rfid and self._rfstate is not None: diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py index a82b84b60d1..137795c684f 100644 --- a/homeassistant/components/alarmdecoder/config_flow.py +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import DEVICE_CLASSES from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback -from .const import ( # pylint: disable=unused-import +from .const import ( CONF_ALT_NIGHT_MODE, CONF_AUTO_BYPASS, CONF_CODE_ARM_REQUIRED, @@ -349,12 +349,18 @@ def _device_already_added(current_entries, user_input, protocol): entry_path = entry.data.get(CONF_DEVICE_PATH) entry_baud = entry.data.get(CONF_DEVICE_BAUD) - if protocol == PROTOCOL_SOCKET: - if user_host == entry_host and user_port == entry_port: - return True + if ( + protocol == PROTOCOL_SOCKET + and user_host == entry_host + and user_port == entry_port + ): + return True - if protocol == PROTOCOL_SERIAL: - if user_baud == entry_baud and user_path == entry_path: - return True + if ( + protocol == PROTOCOL_SERIAL + and user_baud == entry_baud + and user_path == entry_path + ): + return True return False diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index a8aed8dac73..80b9c1261a3 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -1,6 +1,6 @@ """Support for AlarmDecoder sensors (Shows Panel Display).""" +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from .const import SIGNAL_PANEL_MESSAGE @@ -16,7 +16,7 @@ async def async_setup_entry( return True -class AlarmDecoderSensor(Entity): +class AlarmDecoderSensor(SensorEntity): """Representation of an AlarmDecoder keypad.""" def __init__(self): diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json index 2d5f91cf373..8c80adcb3c0 100644 --- a/homeassistant/components/alarmdecoder/translations/hu.json +++ b/homeassistant/components/alarmdecoder/translations/hu.json @@ -1,10 +1,40 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "create_entry": { "default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "protocol": { + "data": { + "host": "Hoszt", + "port": "Port" + } + }, + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "edit_select": "Szerkeszt\u00e9s" + } + }, + "zone_details": { + "data": { + "zone_name": "Z\u00f3na neve" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/id.json b/homeassistant/components/alarmdecoder/translations/id.json new file mode 100644 index 00000000000..39c8282b36f --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/id.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "create_entry": { + "default": "Berhasil terhubung ke AlarmDecoder." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "Laju Baud Perangkat", + "device_path": "Jalur Perangkat", + "host": "Host", + "port": "Port" + }, + "title": "Konfigurasikan pengaturan koneksi" + }, + "user": { + "data": { + "protocol": "Protokol" + }, + "title": "Pilih Protokol AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "Bidang di bawah ini harus berupa bilangan bulat.", + "loop_range": "RF Loop harus merupakan bilangan bulat antara 1 dan 4.", + "loop_rfid": "RF Loop tidak dapat digunakan tanpa RF Serial.", + "relay_inclusive": "Relay Address dan Relay Channel saling tergantung dan harus disertakan bersama-sama." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Mode Malam Alternatif", + "auto_bypass": "Diaktifkan Secara Otomatis", + "code_arm_required": "Kode Diperlukan untuk Mengaktifkan" + }, + "title": "Konfigurasikan AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "Edit" + }, + "description": "Apa yang ingin diedit?", + "title": "Konfigurasikan AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "Nama Zona", + "zone_relayaddr": "Relay Address", + "zone_relaychan": "Relay Channel", + "zone_rfid": "RF Serial", + "zone_type": "Jenis Zona" + }, + "description": "Masukkan detail untuk zona {zone_number}. Untuk menghapus zona {zone_number}, kosongkan Nama Zona.", + "title": "Konfigurasikan AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Nomor Zona" + }, + "description": "Masukkan nomor zona yang ingin ditambahkan, diedit, atau dihapus.", + "title": "Konfigurasikan AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/ko.json b/homeassistant/components/alarmdecoder/translations/ko.json index 08383d37151..cdb63a01bfb 100644 --- a/homeassistant/components/alarmdecoder/translations/ko.json +++ b/homeassistant/components/alarmdecoder/translations/ko.json @@ -12,62 +12,62 @@ "step": { "protocol": { "data": { - "device_baudrate": "\uc7a5\uce58 \uc804\uc1a1 \uc18d\ub3c4", - "device_path": "\uc7a5\uce58 \uacbd\ub85c", + "device_baudrate": "\uae30\uae30 \uc804\uc1a1 \uc18d\ub3c4", + "device_path": "\uae30\uae30 \uacbd\ub85c", "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" }, - "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131" + "title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131\ud558\uae30" }, "user": { "data": { "protocol": "\ud504\ub85c\ud1a0\ucf5c" }, - "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd" + "title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd\ud558\uae30" } } }, "options": { "error": { "int": "\uc544\ub798 \ud544\ub4dc\ub294 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", - "loop_range": "RF \ub8e8\ud504\ub294 1\uc5d0\uc11c 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", - "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc\uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", - "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc11c\ub85c \uc758\uc874\uc801\uc774\uba70 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4." + "loop_range": "RF \ub8e8\ud504\ub294 1\uacfc 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc \uc5c6\uc73c\uba74 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc0c1\ud638 \uc758\uc874\uc801\uc774\uae30 \ub54c\ubb38\uc5d0 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "step": { "arm_settings": { "data": { "alt_night_mode": "\ub300\uccb4 \uc57c\uac04 \ubaa8\ub4dc", - "auto_bypass": "\uacbd\ube44\uc911 \uc790\ub3d9 \uc6b0\ud68c", + "auto_bypass": "\uacbd\ube44 \uc911 \uc790\ub3d9 \uc6b0\ud68c", "code_arm_required": "\uacbd\ube44\uc5d0 \ud544\uc694\ud55c \ucf54\ub4dc" }, - "title": "AlarmDecoder \uad6c\uc131" + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" }, "init": { "data": { - "edit_select": "\ud3b8\uc9d1" + "edit_select": "\ud3b8\uc9d1\ud558\uae30" }, - "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "AlarmDecoder \uad6c\uc131" + "description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" }, "zone_details": { "data": { "zone_loop": "RF \ub8e8\ud504", - "zone_name": "\uc601\uc5ed \uc774\ub984", + "zone_name": "\uad6c\uc5ed \uc774\ub984", "zone_relayaddr": "\ub9b4\ub808\uc774 \uc8fc\uc18c", "zone_relaychan": "\ub9b4\ub808\uc774 \ucc44\ub110", "zone_rfid": "RF \uc2dc\ub9ac\uc5bc", - "zone_type": "\uc601\uc5ed \uc720\ud615" + "zone_type": "\uad6c\uc5ed \uc720\ud615" }, - "description": "{zone_number} \uc601\uc5ed\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud569\ub2c8\ub2e4. {zone_number} \uc601\uc5ed\uc744 \uc0ad\uc81c\ud558\ub824\uba74 \uc601\uc5ed \uc774\ub984\uc744 \ube44\uc6cc \ub461\ub2c8\ub2e4.", - "title": "AlarmDecoder \uad6c\uc131" + "description": "\uad6c\uc5ed {zone_number}\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uad6c\uc5ed {zone_number}\uc744(\ub97c) \uc0ad\uc81c\ud558\ub824\uba74 \uad6c\uc5ed \uc774\ub984\uc744 \ube44\uc6cc\ub450\uc138\uc694.", + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" }, "zone_select": { "data": { "zone_number": "\uad6c\uc5ed \ubc88\ud638" }, - "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uc601\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.", - "title": "AlarmDecoder \uad6c\uc131" + "description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uad6c\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "AlarmDecoder \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index 7645b642d59..dfe51df7531 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Alert state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -10,8 +12,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 @@ -21,11 +22,11 @@ VALID_STATES = {STATE_ON, STATE_OFF} async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -58,11 +59,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Alert states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 70d426905e9..d388a22983f 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, + CONF_DESCRIPTION, CONF_NAME, CONF_PASSWORD, ) @@ -12,7 +13,6 @@ from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http from .const import ( CONF_AUDIO, - CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES, CONF_DISPLAY_URL, CONF_ENDPOINT, diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index acfba91a933..69acf95e207 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,6 +1,7 @@ """Alexa capabilities.""" +from __future__ import annotations + import logging -from typing import List, Optional from homeassistant.components import ( cover, @@ -72,7 +73,7 @@ class AlexaCapability: supported_locales = {"en-US"} - def __init__(self, entity: State, instance: Optional[str] = None): + def __init__(self, entity: State, instance: str | None = None): """Initialize an Alexa capability.""" self.entity = entity self.instance = instance @@ -82,7 +83,7 @@ class AlexaCapability: raise NotImplementedError @staticmethod - def properties_supported() -> List[dict]: + def properties_supported() -> list[dict]: """Return what properties this entity supports.""" return [] diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index a076fdcad9e..de8a4a6fdc4 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -40,7 +40,6 @@ API_SCOPE = "scope" API_CHANGE = "change" API_PASSWORD = "password" -CONF_DESCRIPTION = "description" CONF_DISPLAY_CATEGORIES = "display_categories" CONF_SUPPORTED_LOCALES = ( "de-DE", diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index c05d9641b9a..e7eaeb4a1cb 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,6 +1,8 @@ """Alexa entity adapters.""" +from __future__ import annotations + import logging -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from homeassistant.components import ( alarm_control_panel, @@ -30,6 +32,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CLOUD_NEVER_EXPOSED_ENTITIES, + CONF_DESCRIPTION, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -72,7 +75,7 @@ from .capabilities import ( AlexaTimeHoldController, AlexaToggleController, ) -from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES +from .const import CONF_DISPLAY_CATEGORIES if TYPE_CHECKING: from .config import AbstractConfig @@ -251,7 +254,7 @@ class AlexaEntity: The API handlers should manipulate entities only through this interface. """ - def __init__(self, hass: HomeAssistant, config: "AbstractConfig", entity: State): + def __init__(self, hass: HomeAssistant, config: AbstractConfig, entity: State): """Initialize Alexa Entity.""" self.hass = hass self.config = config @@ -300,7 +303,7 @@ class AlexaEntity: Raises _UnsupportedInterface. """ - def interfaces(self) -> List[AlexaCapability]: + def interfaces(self) -> list[AlexaCapability]: """Return a list of supported interfaces. Used for discovery. The list should contain AlexaInterface instances. @@ -353,7 +356,7 @@ class AlexaEntity: @callback -def async_get_entities(hass, config) -> List[AlexaEntity]: +def async_get_entities(hass, config) -> list[AlexaEntity]: """Return all entities that are supported by Alexa.""" entities = [] for state in hass.states.async_all(): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index dce4f9f2210..cee4cda562d 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -653,10 +653,9 @@ def temperature_from_object(hass, temp_obj, interval=False): if temp_obj["scale"] == "FAHRENHEIT": from_unit = TEMP_FAHRENHEIT - elif temp_obj["scale"] == "KELVIN": + elif temp_obj["scale"] == "KELVIN" and not interval: # convert to Celsius if absolute temperature - if not interval: - temp -= 273.15 + temp -= 273.15 return convert_temperature(temp, from_unit, to_unit, interval) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index c34dc34f0dd..712a08ac6b9 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -1,8 +1,9 @@ """Alexa state report code.""" +from __future__ import annotations + import asyncio import json import logging -from typing import Optional import aiohttp import async_timeout @@ -45,8 +46,8 @@ async def async_enable_proactive_mode(hass, smart_home_config): async def async_entity_state_listener( changed_entity: str, - old_state: Optional[State], - new_state: Optional[State], + old_state: State | None, + new_state: State | None, ): if not hass.is_running: return diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index b9f75ff8c6b..554a4aa47bc 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -1,9 +1,10 @@ """Support for Almond.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging import time -from typing import Optional from aiohttp import ClientError, ClientSession import async_timeout @@ -281,7 +282,7 @@ class AlmondAgent(conversation.AbstractConversationAgent): return True async def async_process( - self, text: str, context: Context, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: str | None = None ) -> intent.IntentResponse: """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 8eb5478ecef..548471a664c 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -5,8 +5,8 @@ "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, "hassio_confirm": { - "title": "Almond via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?" + "title": "Almond via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?" } }, "abort": { diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json index 8ba96e603f5..3f9ce635338 100644 --- a/homeassistant/components/almond/translations/ca.json +++ b/homeassistant/components/almond/translations/ca.json @@ -9,7 +9,7 @@ "step": { "hassio_confirm": { "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement de Hass.io: {addon}?", - "title": "Almond (complement de Hass.io)" + "title": "Almond via complement de Hass.io" }, "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" diff --git a/homeassistant/components/almond/translations/cs.json b/homeassistant/components/almond/translations/cs.json index 1c667c9d55e..dc981403ad2 100644 --- a/homeassistant/components/almond/translations/cs.json +++ b/homeassistant/components/almond/translations/cs.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed hass.io {addon}?", - "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed Supervisor {addon}?", + "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" }, "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/almond/translations/da.json b/homeassistant/components/almond/translations/da.json index 37c66ea8efd..0e7a804acc6 100644 --- a/homeassistant/components/almond/translations/da.json +++ b/homeassistant/components/almond/translations/da.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Hass.io-tilf\u00f8jelsen: {addon}?", - "title": "Almond via Hass.io-tilf\u00f8jelse" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Supervisor-tilf\u00f8jelsen: {addon}?", + "title": "Almond via Supervisor-tilf\u00f8jelse" }, "pick_implementation": { "title": "V\u00e6lg godkendelsesmetode" diff --git a/homeassistant/components/almond/translations/de.json b/homeassistant/components/almond/translations/de.json index 5eb8c4940aa..1f69b1c09e4 100644 --- a/homeassistant/components/almond/translations/de.json +++ b/homeassistant/components/almond/translations/de.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Hass.io-Add-On hergestellt wird: {addon}?", - "title": "Almond \u00fcber das Hass.io Add-on" + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Supervisor-Add-On hergestellt wird: {addon}?", + "title": "Almond \u00fcber das Supervisor Add-on" }, "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" diff --git a/homeassistant/components/almond/translations/es-419.json b/homeassistant/components/almond/translations/es-419.json index 50a43d67b6d..ce1d655d69e 100644 --- a/homeassistant/components/almond/translations/es-419.json +++ b/homeassistant/components/almond/translations/es-419.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon}?", - "title": "Almond a trav\u00e9s del complemento Hass.io" + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon}?", + "title": "Almond a trav\u00e9s del complemento Supervisor" }, "pick_implementation": { "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index f14d3cd04ee..4dc5e4ee1c0 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon} ?", - "title": "Almond a trav\u00e9s del complemento Hass.io" + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon} ?", + "title": "Almond a trav\u00e9s del complemento Supervisor" }, "pick_implementation": { "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" diff --git a/homeassistant/components/almond/translations/et.json b/homeassistant/components/almond/translations/et.json index 55ab29b2a81..c8646d6f090 100644 --- a/homeassistant/components/almond/translations/et.json +++ b/homeassistant/components/almond/translations/et.json @@ -9,7 +9,7 @@ "step": { "hassio_confirm": { "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub Hass.io lisandmoodul: {addon} ?", - "title": "Almond Hass.io lisandmooduli kaudu" + "title": "Almond Hass.io lisandmooduli abil" }, "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json index e4fb8610cd0..0e6a8e0be3f 100644 --- a/homeassistant/components/almond/translations/fr.json +++ b/homeassistant/components/almond/translations/fr.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Voulez-vous configurer Home Assistant pour se connecter \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", - "title": "Almonf via le module compl\u00e9mentaire Hass.io" + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", + "title": "Amande via le module compl\u00e9mentaire Hass.io" }, "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" diff --git a/homeassistant/components/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json index 7654f66bc28..568cd7270de 100644 --- a/homeassistant/components/almond/translations/hu.json +++ b/homeassistant/components/almond/translations/hu.json @@ -1,16 +1,18 @@ { "config": { "abort": { - "cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.", - "missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t." + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "hassio_confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", - "title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Supervisor kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", + "title": "Almond a Supervisor kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" }, "pick_implementation": { - "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/almond/translations/id.json b/homeassistant/components/almond/translations/id.json new file mode 100644 index 00000000000..21a627132c4 --- /dev/null +++ b/homeassistant/components/almond/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung", + "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." + }, + "step": { + "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on Supervisor {addon}?", + "title": "Almond melalui add-on Home Assistant" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json index ab5d7f86f19..7a41f00437b 100644 --- a/homeassistant/components/almond/translations/it.json +++ b/homeassistant/components/almond/translations/it.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant a connettersi ad Almond tramite il componente aggiuntivo Hass.io: {addon} ?", + "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" }, "pick_implementation": { diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json index 062ef885c70..cd9d4d67874 100644 --- a/homeassistant/components/almond/translations/ko.json +++ b/homeassistant/components/almond/translations/ko.json @@ -4,11 +4,11 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\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.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "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?", + "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" }, "pick_implementation": { diff --git a/homeassistant/components/almond/translations/lb.json b/homeassistant/components/almond/translations/lb.json index 5a59645cdaa..0e29d69bbed 100644 --- a/homeassistant/components/almond/translations/lb.json +++ b/homeassistant/components/almond/translations/lb.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der hass.io Erweiderung {addon} bereet gestallt g\u00ebtt?", - "title": "Almond via Hass.io Erweiderung" + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der Supervisor Erweiderung {addon} bereet gestallt g\u00ebtt?", + "title": "Almond via Supervisor Erweiderung" }, "pick_implementation": { "title": "Wiel Authentifikatiouns Method aus" diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json index 26bbc8dea87..43d90100e93 100644 --- a/homeassistant/components/almond/translations/nl.json +++ b/homeassistant/components/almond/translations/nl.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "cannot_connect": "Kan geen verbinding maken met de Almond-server.", - "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond.", + "cannot_connect": "Kan geen verbinding maken", + "missing_configuration": "De Netatmo-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 hass.io add-on {addon} ?", - "title": "Almond via Hass.io add-on" + "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" }, "pick_implementation": { - "title": "Kies de authenticatie methode" + "title": "Kies een authenticatie methode" } } } diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 9cd22ca5bc5..84a57a42ff7 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io-tillegg: {addon}?", + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Supervisor-tillegg: {addon}?", "title": "Almond via Hass.io-tillegg" }, "pick_implementation": { diff --git a/homeassistant/components/almond/translations/sl.json b/homeassistant/components/almond/translations/sl.json index 573df43876f..cb20393201f 100644 --- a/homeassistant/components/almond/translations/sl.json +++ b/homeassistant/components/almond/translations/sl.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Hass.io: {addon} ?", - "title": "Almond prek dodatka Hass.io" + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Supervisor: {addon} ?", + "title": "Almond prek dodatka Supervisor" }, "pick_implementation": { "title": "Izberite na\u010din preverjanja pristnosti" diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json index 6a7dfdb970c..8b20512df9b 100644 --- a/homeassistant/components/almond/translations/sv.json +++ b/homeassistant/components/almond/translations/sv.json @@ -6,8 +6,8 @@ }, "step": { "hassio_confirm": { - "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Hass.io-till\u00e4gget: {addon} ?", - "title": "Almond via Hass.io-till\u00e4gget" + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Supervisor-till\u00e4gget: {addon} ?", + "title": "Almond via Supervisor-till\u00e4gget" }, "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json index 7f8c12917bb..db96ef3d0a3 100644 --- a/homeassistant/components/almond/translations/uk.json +++ b/homeassistant/components/almond/translations/uk.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", + "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" }, "pick_implementation": { "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index 6312d4ecd18..c8004ecde4f 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\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 Almond\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 Almond" + "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" }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 0d0aec47915..0788772a45b 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -6,10 +6,9 @@ from alpha_vantage.foreignexchange import ForeignExchange from alpha_vantage.timeseries import TimeSeries import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Setup completed") -class AlphaVantageSensor(Entity): +class AlphaVantageSensor(SensorEntity): """Representation of a Alpha Vantage sensor.""" def __init__(self, timeseries, symbol): @@ -133,7 +132,7 @@ class AlphaVantageSensor(Entity): return self.values["1. open"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.values is not None: return { @@ -156,7 +155,7 @@ class AlphaVantageSensor(Entity): _LOGGER.debug("Received new values for symbol %s", self._symbol) -class AlphaVantageForeignExchange(Entity): +class AlphaVantageForeignExchange(SensorEntity): """Sensor for foreign exchange rates.""" def __init__(self, foreign_exchange, config): @@ -193,7 +192,7 @@ class AlphaVantageForeignExchange(Entity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.values is not None: return { diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 85504971489..a6dbe60a761 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -38,7 +38,7 @@ def register_flow_implementation(hass, client_id, client_secret): } -@config_entries.HANDLERS.register("ambiclimate") +@config_entries.HANDLERS.register(DOMAIN) class AmbiclimateFlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 19f706be1c8..04035f04cca 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + }, "create_entry": { - "default": "Sikeres autentik\u00e1ci\u00f3" + "default": "Sikeres hiteles\u00edt\u00e9s" } } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/id.json b/homeassistant/components/ambiclimate/translations/id.json new file mode 100644 index 00000000000..66c30afcb09 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "Terjadi kesalahan yang tidak diketahui saat membuat token akses.", + "already_configured": "Akun sudah dikonfigurasi", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "error": { + "follow_link": "Buka tautan dan autentikasi sebelum menekan Kirim", + "no_token": "Tidak diautentikasi dengan Ambiclimate" + }, + "step": { + "auth": { + "description": "Buka [tautan ini] ({authorization_url}) dan **Izinkan** akses ke akun Ambiclimate Anda, lalu kembali dan tekan **Kirim** di bawah ini.\n(Pastikan URL panggil balik yang ditentukan adalah {cb_url})", + "title": "Autentikasi Ambiclimate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/ko.json b/homeassistant/components/ambiclimate/translations/ko.json index c28affbebb3..e47c5041728 100644 --- a/homeassistant/components/ambiclimate/translations/ko.json +++ b/homeassistant/components/ambiclimate/translations/ko.json @@ -10,11 +10,11 @@ }, "error": { "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", - "no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" + "no_token": "Ambi Climate\ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" }, "step": { "auth": { - "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambiclimate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n(\ucf5c\ubc31 URL \uc774 {cb_url} \ub85c \uc9c0\uc815\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", + "description": "[\ub9c1\ud06c]({authorization_url})(\uc744)\ub97c \ud074\ub9ad\ud558\uc5ec Ambiclimate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n(\ucf5c\ubc31 URL\uc774 {cb_url}(\uc73c)\ub85c \uc9c0\uc815\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", "title": "Ambi Climate \uc778\uc99d\ud558\uae30" } } diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json index 1d7652a370e..4e6c5ebb202 100644 --- a/homeassistant/components/ambiclimate/translations/nl.json +++ b/homeassistant/components/ambiclimate/translations/nl.json @@ -6,7 +6,7 @@ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { - "default": "Succesvol geverifieerd met Ambiclimate" + "default": "Succesvol geauthenticeerd" }, "error": { "follow_link": "Gelieve de link te volgen en te verifi\u00ebren voordat u op Verzenden drukt.", diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 4a5558c5963..9d3359ca981 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -5,7 +5,11 @@ from aioambient import Client from aioambient.errors import WebsocketError import voluptuous as vol -from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DOMAIN as BINARY_SENSOR, +) +from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_LOCATION, @@ -14,6 +18,13 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_API_KEY, DEGREE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO2, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, EVENT_HOMEASSISTANT_STOP, IRRADIATION_WATTS_PER_SQUARE_METER, LIGHT_LUX, @@ -39,10 +50,10 @@ from .const import ( DATA_CLIENT, DOMAIN, LOGGER, - TYPE_BINARY_SENSOR, - TYPE_SENSOR, ) +PLATFORMS = [BINARY_SENSOR, SENSOR] + DATA_CONFIG = "config" DEFAULT_SOCKET_MIN_RETRY = 15 @@ -60,6 +71,7 @@ TYPE_BATT6 = "batt6" TYPE_BATT7 = "batt7" TYPE_BATT8 = "batt8" TYPE_BATT9 = "batt9" +TYPE_BATT_CO2 = "batt_co2" TYPE_BATTOUT = "battout" TYPE_CO2 = "co2" TYPE_DAILYRAININ = "dailyrainin" @@ -82,6 +94,12 @@ TYPE_HUMIDITYIN = "humidityin" TYPE_LASTRAIN = "lastRain" TYPE_MAXDAILYGUST = "maxdailygust" TYPE_MONTHLYRAININ = "monthlyrainin" +TYPE_PM25 = "pm25" +TYPE_PM25_24H = "pm25_24h" +TYPE_PM25_BATT = "batt_25" +TYPE_PM25_IN = "pm25_in" +TYPE_PM25_IN_24H = "pm25_in_24h" +TYPE_PM25IN_BATT = "batt_25in" TYPE_RELAY1 = "relay1" TYPE_RELAY10 = "relay10" TYPE_RELAY2 = "relay2" @@ -128,8 +146,6 @@ TYPE_TEMPF = "tempf" TYPE_TEMPINF = "tempinf" TYPE_TOTALRAININ = "totalrainin" TYPE_UV = "uv" -TYPE_PM25 = "pm25" -TYPE_PM25_24H = "pm25_24h" TYPE_WEEKLYRAININ = "weeklyrainin" TYPE_WINDDIR = "winddir" TYPE_WINDDIR_AVG10M = "winddir_avg10m" @@ -141,109 +157,139 @@ TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m" TYPE_WINDSPEEDMPH = "windspeedmph" TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_TYPES = { - TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None), - TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"), - TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"), - TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT3: ("Battery 3", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT4: ("Battery 4", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT5: ("Battery 5", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT6: ("Battery 6", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT7: ("Battery 7", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT8: ("Battery 8", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATT9: ("Battery 9", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_BATTOUT: ("Battery", None, TYPE_BINARY_SENSOR, "battery"), - TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, TYPE_SENSOR, None), - TYPE_DAILYRAININ: ("Daily Rain", "in", TYPE_SENSOR, None), - TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None), - TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None), - TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY: ("Humidity", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"), - TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), - TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None), - TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), - TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILTEMP10F: ("Soil Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP3F: ("Soil Temp 3", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP4F: ("Soil Temp 4", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP5F: ("Soil Temp 5", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP6F: ("Soil Temp 6", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_SOLARRADIATION: ( - "Solar Rad", - IRRADIATION_WATTS_PER_SQUARE_METER, - TYPE_SENSOR, - None, - ), - TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", LIGHT_LUX, TYPE_SENSOR, "illuminance"), - TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP3F: ("Temp 3", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP4F: ("Temp 4", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP5F: ("Temp 5", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP6F: ("Temp 6", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP7F: ("Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP8F: ("Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), - TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None), - TYPE_UV: ("uv", "Index", TYPE_SENSOR, None), - TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, TYPE_SENSOR, None), + TYPE_24HOURRAININ: ("24 Hr Rain", "in", SENSOR, None), + TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE), + TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE), + TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT1: ("Battery 1", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT2: ("Battery 2", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT3: ("Battery 3", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT4: ("Battery 4", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT5: ("Battery 5", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT6: ("Battery 6", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT7: ("Battery 7", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT8: ("Battery 8", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT9: ("Battery 9", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2), + TYPE_DAILYRAININ: ("Daily Rain", "in", SENSOR, None), + TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_EVENTRAININ: ("Event Rain", "in", SENSOR, None), + TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", SENSOR, None), + TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITY: ("Humidity", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP), + TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_MONTHLYRAININ: ("Monthly Rain", "in", SENSOR, None), TYPE_PM25_24H: ( "PM25 24h Avg", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - TYPE_SENSOR, + SENSOR, None, ), - TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None), - TYPE_WINDDIR: ("Wind Dir", DEGREE, TYPE_SENSOR, None), - TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, TYPE_SENSOR, None), - TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), - TYPE_WINDGUSTDIR: ("Gust Dir", DEGREE, TYPE_SENSOR, None), - TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), - TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), - TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), - TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), - TYPE_YEARLYRAININ: ("Yearly Rain", "in", TYPE_SENSOR, None), + TYPE_PM25_BATT: ("PM25 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY), + TYPE_PM25_IN: ( + "PM25 Indoor", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + SENSOR, + None, + ), + TYPE_PM25_IN_24H: ( + "PM25 Indoor 24h Avg", + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + SENSOR, + None, + ), + TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, SENSOR, None), + TYPE_PM25IN_BATT: ( + "PM25 Indoor Battery", + None, + BINARY_SENSOR, + DEVICE_CLASS_BATTERY, + ), + TYPE_RELAY10: ("Relay 10", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY1: ("Relay 1", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY2: ("Relay 2", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY3: ("Relay 3", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY4: ("Relay 4", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY5: ("Relay 5", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY6: ("Relay 6", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY7: ("Relay 7", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY8: ("Relay 8", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_RELAY9: ("Relay 9", None, BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY), + TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), + TYPE_SOILTEMP10F: ( + "Soil Temp 10", + TEMP_FAHRENHEIT, + SENSOR, + DEVICE_CLASS_TEMPERATURE, + ), + TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP3F: ("Soil Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP4F: ("Soil Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP5F: ("Soil Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP6F: ("Soil Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_SOLARRADIATION: ( + "Solar Rad", + IRRADIATION_WATTS_PER_SQUARE_METER, + SENSOR, + None, + ), + TYPE_SOLARRADIATION_LX: ( + "Solar Rad (lx)", + LIGHT_LUX, + SENSOR, + DEVICE_CLASS_ILLUMINANCE, + ), + TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP3F: ("Temp 3", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP4F: ("Temp 4", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP5F: ("Temp 5", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP6F: ("Temp 6", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP7F: ("Temp 7", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP8F: ("Temp 8", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), + TYPE_TOTALRAININ: ("Lifetime Rain", "in", SENSOR, None), + TYPE_UV: ("uv", "Index", SENSOR, None), + TYPE_WEEKLYRAININ: ("Weekly Rain", "in", SENSOR, None), + TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None), + TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None), + TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDGUSTDIR: ("Gust Dir", DEGREE, SENSOR, None), + TYPE_WINDGUSTMPH: ("Wind Gust", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None), + TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None), } CONFIG_SCHEMA = vol.Schema( @@ -260,13 +306,12 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): - """Set up the Ambient PWS component.""" + """Set up the Ambient PWS integration.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_CLIENT] = {} if DOMAIN not in config: return True - conf = config[DOMAIN] # Store config for use during entry setup: @@ -289,7 +334,6 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_update_entry( config_entry, unique_id=config_entry.data[CONF_APP_KEY] ) - session = aiohttp_client.async_get_clientsession(hass) try: @@ -322,8 +366,8 @@ async def async_unload_entry(hass, config_entry): hass.async_create_task(ambient.ws_disconnect()) tasks = [ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in ("binary_sensor", "sensor") + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] await asyncio.gather(*tasks) @@ -347,7 +391,6 @@ async def async_migrate_entry(hass, config_entry): version = config_entry.version = 2 hass.config_entries.async_update_entry(config_entry) - LOGGER.info("Migration to version %s successful", version) return True @@ -405,7 +448,6 @@ class AmbientStation: for station in data["devices"]: if station["macAddress"] in self.stations: continue - LOGGER.debug("New station subscription: %s", data) # Only create entities based on the data coming through the socket. @@ -416,7 +458,6 @@ class AmbientStation: ] if TYPE_SOLARRADIATION in monitored_conditions: monitored_conditions.append(TYPE_SOLARRADIATION_LX) - self.stations[station["macAddress"]] = { ATTR_LAST_DATA: station["lastData"], ATTR_LOCATION: station.get("info", {}).get("location"), @@ -425,20 +466,18 @@ class AmbientStation: "name", station["macAddress"] ), } - # If the websocket disconnects and reconnects, the on_subscribed # handler will get called again; in that case, we don't want to # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - for component in ("binary_sensor", "sensor"): + for platform in PLATFORMS: self._hass.async_create_task( self._hass.config_entries.async_forward_entry_setup( - self._config_entry, component + self._config_entry, platform ) ) self._entry_setup_complete = True - self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client.websocket.on_connect(on_connect) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 0a3a91e515c..c2e5ad8b4f4 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -1,5 +1,8 @@ """Support for Ambient Weather Station binary sensors.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR, + BinarySensorEntity, +) from homeassistant.const import ATTR_NAME from homeassistant.core import callback @@ -15,16 +18,13 @@ from . import ( TYPE_BATT8, TYPE_BATT9, TYPE_BATT10, + TYPE_BATT_CO2, TYPE_BATTOUT, + TYPE_PM25_BATT, + TYPE_PM25IN_BATT, AmbientWeatherEntity, ) -from .const import ( - ATTR_LAST_DATA, - ATTR_MONITORED_CONDITIONS, - DATA_CLIENT, - DOMAIN, - TYPE_BINARY_SENSOR, -) +from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN async def async_setup_entry(hass, entry, async_add_entities): @@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities): for mac_address, station in ambient.stations.items(): for condition in station[ATTR_MONITORED_CONDITIONS]: name, _, kind, device_class = SENSOR_TYPES[condition] - if kind == TYPE_BINARY_SENSOR: + if kind == BINARY_SENSOR: binary_sensor_list.append( AmbientWeatherBinarySensor( ambient, @@ -67,7 +67,10 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): TYPE_BATT7, TYPE_BATT8, TYPE_BATT9, + TYPE_BATT_CO2, TYPE_BATTOUT, + TYPE_PM25_BATT, + TYPE_PM25IN_BATT, ): return self._state == 0 diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index a4c0a6aa44f..30548bbe31b 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.helpers import aiohttp_client -from .const import CONF_APP_KEY, DOMAIN # pylint: disable=unused-import +from .const import CONF_APP_KEY, DOMAIN class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index e59f926eac3..87b5ff61877 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -10,6 +10,3 @@ ATTR_MONITORED_CONDITIONS = "monitored_conditions" CONF_APP_KEY = "app_key" DATA_CLIENT = "data_client" - -TYPE_BINARY_SENSOR = "binary_sensor" -TYPE_SENSOR = "sensor" diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 540e2facd4d..7c60d1da9bc 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station sensors.""" +from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity from homeassistant.const import ATTR_NAME from homeassistant.core import callback @@ -8,13 +9,7 @@ from . import ( TYPE_SOLARRADIATION_LX, AmbientWeatherEntity, ) -from .const import ( - ATTR_LAST_DATA, - ATTR_MONITORED_CONDITIONS, - DATA_CLIENT, - DOMAIN, - TYPE_SENSOR, -) +from .const import ATTR_LAST_DATA, ATTR_MONITORED_CONDITIONS, DATA_CLIENT, DOMAIN async def async_setup_entry(hass, entry, async_add_entities): @@ -25,7 +20,7 @@ async def async_setup_entry(hass, entry, async_add_entities): for mac_address, station in ambient.stations.items(): for condition in station[ATTR_MONITORED_CONDITIONS]: name, unit, kind, device_class = SENSOR_TYPES[condition] - if kind == TYPE_SENSOR: + if kind == SENSOR: sensor_list.append( AmbientWeatherSensor( ambient, @@ -41,7 +36,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensor_list, True) -class AmbientWeatherSensor(AmbientWeatherEntity): +class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): """Define an Ambient sensor.""" def __init__( diff --git a/homeassistant/components/ambient_station/translations/hu.json b/homeassistant/components/ambient_station/translations/hu.json index e6b95634827..7c7e3a658b9 100644 --- a/homeassistant/components/ambient_station/translations/hu.json +++ b/homeassistant/components/ambient_station/translations/hu.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, "error": { - "invalid_key": "\u00c9rv\u00e9nytelen API kulcs \u00e9s / vagy alkalmaz\u00e1skulcs", + "invalid_key": "\u00c9rv\u00e9nytelen API kulcs", "no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z" }, "step": { diff --git a/homeassistant/components/ambient_station/translations/id.json b/homeassistant/components/ambient_station/translations/id.json new file mode 100644 index 00000000000..1b5a1dd0b21 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "invalid_key": "Kunci API tidak valid", + "no_devices": "Tidak ada perangkat yang ditemukan dalam akun" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "app_key": "Kunci Aplikasi" + }, + "title": "Isi informasi Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/nl.json b/homeassistant/components/ambient_station/translations/nl.json index 02c8f0727f8..008bc10e084 100644 --- a/homeassistant/components/ambient_station/translations/nl.json +++ b/homeassistant/components/ambient_station/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Service is al geconfigureerd" }, "error": { - "invalid_key": "Ongeldige API-sleutel en/of applicatiesleutel", + "invalid_key": "Ongeldige API-sleutel", "no_devices": "Geen apparaten gevonden in account" }, "step": { diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 3baad1ac88e..71c277e578c 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -1,4 +1,5 @@ """Support for Amcrest IP cameras.""" +from contextlib import suppress from datetime import timedelta import logging import threading @@ -191,10 +192,8 @@ class AmcrestChecker(Http): def _wrap_test_online(self, now): """Test if camera is back online.""" _LOGGER.debug("Testing if %s back online", self._wrap_name) - try: - self.current_time - except AmcrestError: - pass + with suppress(AmcrestError): + self.current_time # pylint: disable=pointless-statement def _monitor_events(hass, name, api, event_codes): diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 649258c42c7..0add382b81f 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Amcrest IP camera binary sensors.""" +from contextlib import suppress from datetime import timedelta import logging @@ -38,6 +39,8 @@ BINARY_SENSOR_AUDIO_DETECTED_POLLED = "audio_detected_polled" BINARY_SENSOR_MOTION_DETECTED = "motion_detected" BINARY_SENSOR_MOTION_DETECTED_POLLED = "motion_detected_polled" BINARY_SENSOR_ONLINE = "online" +BINARY_SENSOR_CROSSLINE_DETECTED = "crossline_detected" +BINARY_SENSOR_CROSSLINE_DETECTED_POLLED = "crossline_detected_polled" BINARY_POLLED_SENSORS = [ BINARY_SENSOR_AUDIO_DETECTED_POLLED, BINARY_SENSOR_MOTION_DETECTED_POLLED, @@ -45,11 +48,18 @@ BINARY_POLLED_SENSORS = [ ] _AUDIO_DETECTED_PARAMS = ("Audio Detected", DEVICE_CLASS_SOUND, "AudioMutation") _MOTION_DETECTED_PARAMS = ("Motion Detected", DEVICE_CLASS_MOTION, "VideoMotion") +_CROSSLINE_DETECTED_PARAMS = ( + "CrossLine Detected", + DEVICE_CLASS_MOTION, + "CrossLineDetection", +) BINARY_SENSORS = { BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS, BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS, BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS, BINARY_SENSOR_MOTION_DETECTED_POLLED: _MOTION_DETECTED_PARAMS, + BINARY_SENSOR_CROSSLINE_DETECTED: _CROSSLINE_DETECTED_PARAMS, + BINARY_SENSOR_CROSSLINE_DETECTED_POLLED: _CROSSLINE_DETECTED_PARAMS, BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None), } BINARY_SENSORS = { @@ -58,6 +68,7 @@ BINARY_SENSORS = { } _EXCLUSIVE_OPTIONS = [ {BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED}, + {BINARY_SENSOR_CROSSLINE_DETECTED, BINARY_SENSOR_CROSSLINE_DETECTED_POLLED}, ] _UPDATE_MSG = "Updating %s binary sensor" @@ -144,10 +155,8 @@ class AmcrestBinarySensor(BinarySensorEntity): # Send a command to the camera to test if we can still communicate with it. # Override of Http.command() in __init__.py will set self._api.available # accordingly. - try: - self._api.current_time - except AmcrestError: - pass + with suppress(AmcrestError): + self._api.current_time # pylint: disable=pointless-statement self._state = self._api.available def _update_others(self): diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index a8aabc233d1..f57b9e62bae 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -197,7 +197,7 @@ class AmcrestCam(Camera): # and before initiating shapshot. while self._snapshot_task: self._check_snapshot_ok() - _LOGGER.debug("Waiting for previous snapshot from %s ...", self._name) + _LOGGER.debug("Waiting for previous snapshot from %s", self._name) await self._snapshot_task self._check_snapshot_ok() # Run snapshot command in separate Task that can't be cancelled so @@ -266,7 +266,7 @@ class AmcrestCam(Camera): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the Amcrest-specific camera state attributes.""" attr = {} if self._audio_enabled is not None: diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 44ebcfcdb95..a30de62494e 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -4,9 +4,9 @@ import logging from amcrest import AmcrestError +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, CONF_SENSORS, PERCENTAGE from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE from .helpers import log_update_error, service_signal @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class AmcrestSensor(Entity): +class AmcrestSensor(SensorEntity): """A sensor implementation for Amcrest IP camera.""" def __init__(self, name, device, sensor_type): @@ -66,7 +66,7 @@ class AmcrestSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py new file mode 100644 index 00000000000..c1187af7f17 --- /dev/null +++ b/homeassistant/components/analytics/__init__.py @@ -0,0 +1,76 @@ +"""Send instance and usage analytics.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later, async_track_time_interval + +from .analytics import Analytics +from .const import ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA + + +async def async_setup(hass: HomeAssistant, _): + """Set up the analytics integration.""" + analytics = Analytics(hass) + + # Load stored data + await analytics.load() + + async def start_schedule(_event): + """Start the send schedule after the started event.""" + # Wait 15 min after started + async_call_later(hass, 900, analytics.send_analytics) + + # Send every day + async_track_time_interval(hass, analytics.send_analytics, INTERVAL) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule) + + websocket_api.async_register_command(hass, websocket_analytics) + websocket_api.async_register_command(hass, websocket_analytics_preferences) + + hass.data[DOMAIN] = analytics + return True + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required("type"): "analytics"}) +async def websocket_analytics( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Return analytics preferences.""" + analytics: Analytics = hass.data[DOMAIN] + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences}, + ) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "analytics/preferences", + vol.Required("preferences", default={}): PREFERENCE_SCHEMA, + } +) +async def websocket_analytics_preferences( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Update analytics preferences.""" + preferences = msg[ATTR_PREFERENCES] + analytics: Analytics = hass.data[DOMAIN] + + await analytics.save_preferences(preferences) + await analytics.send_analytics() + + connection.send_result( + msg["id"], + {ATTR_PREFERENCES: analytics.preferences}, + ) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py new file mode 100644 index 00000000000..e6e8678cc10 --- /dev/null +++ b/homeassistant/components/analytics/analytics.py @@ -0,0 +1,240 @@ +"""Analytics helper class for the analytics integration.""" +import asyncio +import uuid + +import aiohttp +import async_timeout + +from homeassistant.components import hassio +from homeassistant.components.api import ATTR_INSTALLATION_TYPE +from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.storage import Store +from homeassistant.helpers.system_info import async_get_system_info +from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.setup import async_get_loaded_integrations + +from .const import ( + ANALYTICS_ENDPOINT_URL, + ATTR_ADDON_COUNT, + ATTR_ADDONS, + ATTR_AUTO_UPDATE, + ATTR_AUTOMATION_COUNT, + ATTR_BASE, + ATTR_CUSTOM_INTEGRATIONS, + ATTR_DIAGNOSTICS, + ATTR_HEALTHY, + ATTR_INTEGRATION_COUNT, + ATTR_INTEGRATIONS, + ATTR_ONBOARDED, + ATTR_PREFERENCES, + ATTR_PROTECTED, + ATTR_SLUG, + ATTR_STATE_COUNT, + ATTR_STATISTICS, + ATTR_SUPERVISOR, + ATTR_SUPPORTED, + ATTR_USAGE, + ATTR_USER_COUNT, + ATTR_UUID, + ATTR_VERSION, + LOGGER, + PREFERENCE_SCHEMA, + STORAGE_KEY, + STORAGE_VERSION, +) + + +class Analytics: + """Analytics helper class for the analytics integration.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the Analytics class.""" + self.hass: HomeAssistant = hass + self.session = async_get_clientsession(hass) + self._data = {ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None} + self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + @property + def preferences(self) -> dict: + """Return the current active preferences.""" + preferences = self._data[ATTR_PREFERENCES] + return { + ATTR_BASE: preferences.get(ATTR_BASE, False), + ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False), + ATTR_USAGE: preferences.get(ATTR_USAGE, False), + ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False), + } + + @property + def onboarded(self) -> bool: + """Return bool if the user has made a choice.""" + return self._data[ATTR_ONBOARDED] + + @property + def uuid(self) -> bool: + """Return the uuid for the analytics integration.""" + return self._data[ATTR_UUID] + + @property + def supervisor(self) -> bool: + """Return bool if a supervisor is present.""" + return hassio.is_hassio(self.hass) + + async def load(self) -> None: + """Load preferences.""" + stored = await self._store.async_load() + if stored: + self._data = stored + + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) + if not self.onboarded: + # User have not configured analytics, get this setting from the supervisor + if supervisor_info[ATTR_DIAGNOSTICS] and not self.preferences.get( + ATTR_DIAGNOSTICS, False + ): + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = True + elif not supervisor_info[ATTR_DIAGNOSTICS] and self.preferences.get( + ATTR_DIAGNOSTICS, False + ): + self._data[ATTR_PREFERENCES][ATTR_DIAGNOSTICS] = False + + async def save_preferences(self, preferences: dict) -> None: + """Save preferences.""" + preferences = PREFERENCE_SCHEMA(preferences) + self._data[ATTR_PREFERENCES].update(preferences) + self._data[ATTR_ONBOARDED] = True + + await self._store.async_save(self._data) + + if self.supervisor: + await hassio.async_update_diagnostics( + self.hass, self.preferences.get(ATTR_DIAGNOSTICS, False) + ) + + async def send_analytics(self, _=None) -> None: + """Send analytics.""" + supervisor_info = None + + if not self.onboarded or not self.preferences.get(ATTR_BASE, False): + LOGGER.debug("Nothing to submit") + return + + if self._data.get(ATTR_UUID) is None: + self._data[ATTR_UUID] = uuid.uuid4().hex + await self._store.async_save(self._data) + + if self.supervisor: + supervisor_info = hassio.get_supervisor_info(self.hass) + + system_info = await async_get_system_info(self.hass) + integrations = [] + custom_integrations = [] + addons = [] + payload: dict = { + ATTR_UUID: self.uuid, + ATTR_VERSION: HA_VERSION, + ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE], + } + + if supervisor_info is not None: + payload[ATTR_SUPERVISOR] = { + ATTR_HEALTHY: supervisor_info[ATTR_HEALTHY], + ATTR_SUPPORTED: supervisor_info[ATTR_SUPPORTED], + } + + if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( + ATTR_STATISTICS, False + ): + configured_integrations = await asyncio.gather( + *[ + async_get_integration(self.hass, domain) + for domain in async_get_loaded_integrations(self.hass) + ], + return_exceptions=True, + ) + + for integration in configured_integrations: + if isinstance(integration, IntegrationNotFound): + continue + + if isinstance(integration, BaseException): + raise integration + + if integration.disabled: + continue + + if not integration.is_built_in: + custom_integrations.append( + { + ATTR_DOMAIN: integration.domain, + ATTR_VERSION: integration.version, + } + ) + continue + + integrations.append(integration.domain) + + if supervisor_info is not None: + installed_addons = await asyncio.gather( + *[ + hassio.async_get_addon_info(self.hass, addon[ATTR_SLUG]) + for addon in supervisor_info[ATTR_ADDONS] + ] + ) + for addon in installed_addons: + addons.append( + { + ATTR_SLUG: addon[ATTR_SLUG], + ATTR_PROTECTED: addon[ATTR_PROTECTED], + ATTR_VERSION: addon[ATTR_VERSION], + ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE], + } + ) + + if self.preferences.get(ATTR_USAGE, False): + payload[ATTR_INTEGRATIONS] = integrations + payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations + if supervisor_info is not None: + payload[ATTR_ADDONS] = addons + + if self.preferences.get(ATTR_STATISTICS, False): + payload[ATTR_STATE_COUNT] = len(self.hass.states.async_all()) + payload[ATTR_AUTOMATION_COUNT] = len( + self.hass.states.async_all(AUTOMATION_DOMAIN) + ) + payload[ATTR_INTEGRATION_COUNT] = len(integrations) + if supervisor_info is not None: + payload[ATTR_ADDON_COUNT] = len(addons) + payload[ATTR_USER_COUNT] = len( + [ + user + for user in await self.hass.auth.async_get_users() + if not user.system_generated + ] + ) + + try: + with async_timeout.timeout(30): + response = await self.session.post(ANALYTICS_ENDPOINT_URL, json=payload) + if response.status == 200: + LOGGER.info( + ( + "Submitted analytics to Home Assistant servers. " + "Information submitted includes %s" + ), + payload, + ) + else: + LOGGER.warning( + "Sending analytics failed with statuscode %s", response.status + ) + except asyncio.TimeoutError: + LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL) + except aiohttp.ClientError as err: + LOGGER.error( + "Error sending analytics to %s: %r", ANALYTICS_ENDPOINT_URL, err + ) diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py new file mode 100644 index 00000000000..a6fe91b5a44 --- /dev/null +++ b/homeassistant/components/analytics/const.py @@ -0,0 +1,48 @@ +"""Constants for the analytics integration.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +ANALYTICS_ENDPOINT_URL = "https://analytics-api.home-assistant.io/v1" +DOMAIN = "analytics" +INTERVAL = timedelta(days=1) +STORAGE_KEY = "core.analytics" +STORAGE_VERSION = 1 + + +LOGGER: logging.Logger = logging.getLogger(__package__) + +ATTR_ADDON_COUNT = "addon_count" +ATTR_ADDONS = "addons" +ATTR_AUTO_UPDATE = "auto_update" +ATTR_AUTOMATION_COUNT = "automation_count" +ATTR_BASE = "base" +ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" +ATTR_DIAGNOSTICS = "diagnostics" +ATTR_HEALTHY = "healthy" +ATTR_INSTALLATION_TYPE = "installation_type" +ATTR_INTEGRATION_COUNT = "integration_count" +ATTR_INTEGRATIONS = "integrations" +ATTR_ONBOARDED = "onboarded" +ATTR_PREFERENCES = "preferences" +ATTR_PROTECTED = "protected" +ATTR_SLUG = "slug" +ATTR_STATE_COUNT = "state_count" +ATTR_STATISTICS = "statistics" +ATTR_SUPERVISOR = "supervisor" +ATTR_SUPPORTED = "supported" +ATTR_USAGE = "usage" +ATTR_USER_COUNT = "user_count" +ATTR_UUID = "uuid" +ATTR_VERSION = "version" + + +PREFERENCE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_BASE): bool, + vol.Optional(ATTR_DIAGNOSTICS): bool, + vol.Optional(ATTR_STATISTICS): bool, + vol.Optional(ATTR_USAGE): bool, + } +) diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json new file mode 100644 index 00000000000..db795501fa6 --- /dev/null +++ b/homeassistant/components/analytics/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "analytics", + "name": "Analytics", + "documentation": "https://www.home-assistant.io/integrations/analytics", + "codeowners": ["@home-assistant/core", "@ludeeus"], + "dependencies": ["api", "websocket_api"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 905d0262862..54a281feacd 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -321,7 +321,7 @@ class AndroidIPCamEntity(Entity): return self._ipcam.available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state_attr = {ATTR_HOST: self._host} if self._ipcam.status_data is None: diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index 05c1fe16c61..adedb297cd1 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -1,4 +1,5 @@ """Support for Android IP Webcam sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.icon import icon_for_battery_level from . import ( @@ -30,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(all_sensors, True) -class IPWebcamSensor(AndroidIPCamEntity): +class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): """Representation of a IP Webcam sensor.""" def __init__(self, name, host, ipcam, sensor): diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 4d17b4ecdad..b2a8ceffc9f 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -469,7 +469,7 @@ class ADBDevice(MediaPlayerEntity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Provide the last ADB command's response and the device's HDMI input as attributes.""" return { "adb_response": self._adb_response, diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 55e6d8c7a56..36dc1155b7f 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -4,7 +4,7 @@ import logging from apcaccess.status import ALL_UNITS import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_RESOURCES, ELECTRICAL_CURRENT_AMPERE, @@ -18,7 +18,6 @@ from homeassistant.const import ( VOLT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DOMAIN @@ -156,7 +155,7 @@ def infer_unit(value): return value, None -class APCUPSdSensor(Entity): +class APCUPSdSensor(SensorEntity): """Representation of a sensor entity for APCUPSd status values.""" def __init__(self, data, sensor_type): diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index e40a9332c38..a91d8540286 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -1,5 +1,6 @@ """Rest API for Home Assistant.""" import asyncio +from contextlib import suppress import json import logging @@ -37,7 +38,6 @@ from homeassistant.helpers import template from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers.state import AsyncTrackStates from homeassistant.helpers.system_info import async_get_system_info _LOGGER = logging.getLogger(__name__) @@ -197,15 +197,11 @@ class APIDiscoveryView(HomeAssistantView): ATTR_VERSION: __version__, } - try: + with suppress(NoURLAvailableError): data["external_url"] = get_url(hass, allow_internal=False) - except NoURLAvailableError: - pass - try: + with suppress(NoURLAvailableError): data["internal_url"] = get_url(hass, allow_external=False) - except NoURLAvailableError: - pass # Set old base URL based on external or internal data["base_url"] = data["external_url"] or data["internal_url"] @@ -367,20 +363,27 @@ class APIDomainServicesView(HomeAssistantView): Returns a list of changed states. """ - hass = request.app["hass"] + hass: ha.HomeAssistant = request.app["hass"] body = await request.text() try: data = json.loads(body) if body else None except ValueError: return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST) - with AsyncTrackStates(hass) as changed_states: - try: - await hass.services.async_call( - domain, service, data, blocking=True, context=self.context(request) - ) - except (vol.Invalid, ServiceNotFound) as ex: - raise HTTPBadRequest() from ex + context = self.context(request) + + try: + await hass.services.async_call( + domain, service, data, blocking=True, context=context + ) + except (vol.Invalid, ServiceNotFound) as ex: + raise HTTPBadRequest() from ex + + changed_states = [] + + for state in hass.states.async_all(): + if state.context is context: + changed_states.append(state) return self.json(changed_states) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index 6f8de7e9c84..c9e12a20863 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -1,4 +1,5 @@ """APNS Notification platform.""" +from contextlib import suppress import logging from apns2.client import APNsClient @@ -155,7 +156,7 @@ class ApnsNotificationService(BaseNotificationService): self.device_states = {} self.topic = topic - try: + with suppress(FileNotFoundError): self.devices = { str(key): ApnsDevice( str(key), @@ -165,8 +166,6 @@ class ApnsNotificationService(BaseNotificationService): ) for (key, value) in load_yaml_config_file(self.yaml_path).items() } - except FileNotFoundError: - pass tracking_ids = [ device.full_tracking_device_id diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index b41a107d126..b4e0e1be666 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -8,6 +8,7 @@ from pyatv.const import Protocol from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_ADDRESS, CONF_NAME, @@ -34,19 +35,12 @@ BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes NOTIFICATION_TITLE = "Apple TV Notification" NOTIFICATION_ID = "apple_tv_notification" -SOURCE_REAUTH = "reauth" - SIGNAL_CONNECTED = "apple_tv_connected" SIGNAL_DISCONNECTED = "apple_tv_disconnected" PLATFORMS = [MP_DOMAIN, REMOTE_DOMAIN] -async def async_setup(hass, config): - """Set up the Apple TV integration.""" - return True - - async def async_setup_entry(hass, entry): """Set up a config entry for Apple TV.""" manager = AppleTVManager(hass, entry) @@ -62,8 +56,8 @@ async def async_setup_entry(hass, entry): """Set up platforms and initiate connection.""" await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS ] ) await manager.init() @@ -151,6 +145,13 @@ class AppleTVEntity(Entity): """No polling needed for Apple TV.""" return False + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._identifier)}, + } + class AppleTVManager: """Connection and power manager for an Apple TV. @@ -181,7 +182,7 @@ class AppleTVManager: This is a callback function from pyatv.interface.DeviceListener. """ _LOGGER.warning( - 'Connection lost to Apple TV "%s"', self.config_entry.data.get(CONF_NAME) + 'Connection lost to Apple TV "%s"', self.config_entry.data[CONF_NAME] ) self._connection_was_lost = True self._handle_disconnect() @@ -268,7 +269,7 @@ class AppleTVManager: """Problem to authenticate occurred that needs intervention.""" _LOGGER.debug("Authentication error, reconfigure integration") - name = self.config_entry.data.get(CONF_NAME) + name = self.config_entry.data[CONF_NAME] identifier = self.config_entry.unique_id self.hass.components.persistent_notification.create( @@ -337,7 +338,8 @@ class AppleTVManager: self._connection_attempts = 0 if self._connection_was_lost: _LOGGER.info( - 'Connection was re-established to Apple TV "%s"', self.atv.service.name + 'Connection was re-established to Apple TV "%s"', + self.config_entry.data[CONF_NAME], ) self._connection_was_lost = False @@ -348,10 +350,16 @@ class AppleTVManager: "name": self.config_entry.data[CONF_NAME], } + area = attrs["name"] + name_trailer = f" {DEFAULT_NAME}" + if area.endswith(name_trailer): + area = area[: -len(name_trailer)] + attrs["suggested_area"] = area + if self.atv: dev_info = self.atv.device_info - attrs["model"] = "Apple TV " + dev_info.model.name.replace("Gen", "") + attrs["model"] = DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") attrs["sw_version"] = dev_info.version if dev_info.mac: diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index ef0a0cfe59e..ab4ced1547f 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -21,8 +21,7 @@ from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,7 @@ async def device_scan(identifier, loop, cache=None): return True if identifier == dev.name: return True - return any([service.identifier == identifier for service in dev.services]) + return any(service.identifier == identifier for service in dev.services) def _host_filter(): try: diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 66ae2864dc4..a60c5db3a06 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ - "pyatv==0.7.6" + "pyatv==0.7.7" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 26c02fabbb4..63bf29a73f1 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -1,15 +1,18 @@ { "config": { "abort": { - "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton", - "unknown": "V\u00e1ratlan hiba" + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "invalid_auth": "Azonos\u00edt\u00e1s nem siker\u00fclt", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "unknown": "V\u00e1ratlan hiba" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "Apple TV: {name}", "step": { "confirm": { "title": "Apple TV sikeresen hozz\u00e1adva" @@ -19,7 +22,7 @@ }, "pair_with_pin": { "data": { - "pin": "PIN K\u00f3d" + "pin": "PIN-k\u00f3d" }, "title": "P\u00e1ros\u00edt\u00e1s" }, diff --git a/homeassistant/components/apple_tv/translations/id.json b/homeassistant/components/apple_tv/translations/id.json new file mode 100644 index 00000000000..5646b498242 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/id.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "backoff": "Perangkat tidak bisa menerima permintaan pemasangan saat ini (Anda mungkin telah berulang kali memasukkan kode PIN yang salah). Coba lagi nanti.", + "device_did_not_pair": "Tidak ada upaya untuk menyelesaikan proses pemasangan dari sisi perangkat.", + "invalid_config": "Konfigurasi untuk perangkat ini tidak lengkap. Coba tambahkan lagi.", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "invalid_auth": "Autentikasi tidak valid", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "no_usable_service": "Perangkat ditemukan tetapi kami tidak dapat mengidentifikasi berbagai cara untuk membuat koneksi ke perangkat tersebut. Jika Anda terus melihat pesan ini, coba tentukan alamat IP-nya atau mulai ulang Apple TV Anda.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Anda akan menambahkan Apple TV bernama `{name}` ke Home Assistant.\n\n** Untuk menyelesaikan proses, Anda mungkin harus memasukkan kode PIN beberapa kali.**\n\nPerhatikan bahwa Anda *tidak* akan dapat mematikan Apple TV dengan integrasi ini. Hanya pemutar media di Home Assistant yang akan dimatikan!", + "title": "Konfirmasikan menambahkan Apple TV" + }, + "pair_no_pin": { + "description": "Pemasangan diperlukan untuk layanan `{protocol}`. Masukkan PIN {pin} di Apple TV untuk melanjutkan.", + "title": "Memasangkan" + }, + "pair_with_pin": { + "data": { + "pin": "Kode PIN" + }, + "description": "Pemasangan diperlukan untuk protokol `{protocol}`. Masukkan kode PIN yang ditampilkan pada layar. Angka nol di awal harus dihilangkan. Misalnya, masukkan 123 jika kode yang ditampilkan adalah 0123.", + "title": "Memasangkan" + }, + "reconfigure": { + "description": "Apple TV ini mengalami masalah koneksi dan harus dikonfigurasi ulang.", + "title": "Konfigurasi ulang perangkat" + }, + "service_problem": { + "description": "Terjadi masalah saat protokol pemasangan `{protocol}`. Masalah ini akan diabaikan.", + "title": "Gagal menambahkan layanan" + }, + "user": { + "data": { + "device_input": "Perangkat" + }, + "description": "Mulai dengan memasukkan nama perangkat (misalnya Dapur atau Kamar Tidur) atau alamat IP Apple TV yang ingin ditambahkan. Jika ada perangkat yang ditemukan secara otomatis di jaringan Anda, perangkat tersebut akan ditampilkan di bawah ini.\n\nJika Anda tidak dapat melihat perangkat atau mengalami masalah, coba tentukan alamat IP perangkat.\n\n{devices}", + "title": "Siapkan Apple TV baru" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Jangan nyalakan perangkat saat memulai Home Assistant" + }, + "description": "Konfigurasikan pengaturan umum perangkat" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ko.json b/homeassistant/components/apple_tv/translations/ko.json index c7e664b0638..278dbec04e4 100644 --- a/homeassistant/components/apple_tv/translations/ko.json +++ b/homeassistant/components/apple_tv/translations/ko.json @@ -3,6 +3,9 @@ "abort": { "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "backoff": "\uae30\uae30\uac00 \ud604\uc7ac \ud398\uc5b4\ub9c1 \uc694\uccad\uc744 \uc218\ub77d\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4(\uc798\ubabb\ub41c PIN \ucf54\ub4dc\ub97c \ub108\ubb34 \ub9ce\uc774 \uc785\ub825\ud588\uc744 \uc218 \uc788\uc74c). \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "device_did_not_pair": "\uae30\uae30\uc5d0\uc11c \ud398\uc5b4\ub9c1 \ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "invalid_config": "\uc774 \uae30\uae30\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \ubd88\uc644\uc804\ud569\ub2c8\ub2e4. \ub2e4\uc2dc \ucd94\uac00\ud574\uc8fc\uc138\uc694.", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, @@ -10,14 +13,52 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_usable_service": "\uae30\uae30\ub97c \ucc3e\uc558\uc9c0\ub9cc \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ubc29\ubc95\uc744 \uc2dd\ubcc4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uba54\uc2dc\uc9c0\uac00 \uacc4\uc18d \ud45c\uc2dc\ub418\uba74 \ud574\ub2f9 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc9c0\uc815\ud574\uc8fc\uc2dc\uac70\ub098 Apple TV\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Apple TV: {name}", "step": { + "confirm": { + "description": "Apple TV `{name}`\uc744(\ub97c) Home Assistant\uc5d0 \ucd94\uac00\ud558\ub824\uace0 \ud569\ub2c8\ub2e4.\n\n**\ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uba74 \uc5ec\ub7ec \uac1c\uc758 PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc57c \ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.**\n\n\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \ud1b5\ud574 Apple TV\uc758 \uc804\uc6d0\uc740 *\ub04c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4*. Home Assistant\uc758 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\ub9cc \uaebc\uc9d1\ub2c8\ub2e4!", + "title": "Apple TV \ucd94\uac00 \ud655\uc778\ud558\uae30" + }, + "pair_no_pin": { + "description": "`{protocol}` \uc11c\ube44\uc2a4\uc5d0 \ub300\ud55c \ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc18d\ud558\ub824\uba74 Apple TV\uc5d0 PIN {pin}\uc744(\ub97c) \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ud398\uc5b4\ub9c1\ud558\uae30" + }, "pair_with_pin": { "data": { "pin": "PIN \ucf54\ub4dc" - } + }, + "description": "`{protocol}` \ud504\ub85c\ud1a0\ucf5c\uc5d0 \ub300\ud55c \ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub418\ub294 PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc55e\uc790\ub9ac\uc758 0\uc740 \uc0dd\ub7b5\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc989, \ud45c\uc2dc\ub41c \ucf54\ub4dc\uac00 0123\uc774\uba74 123\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ud398\uc5b4\ub9c1\ud558\uae30" + }, + "reconfigure": { + "description": "\uc774 Apple TV\uc5d0 \uc5f0\uacb0 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud558\uc5ec \ub2e4\uc2dc \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "\uae30\uae30 \uc7ac\uad6c\uc131" + }, + "service_problem": { + "description": "\ud504\ub85c\ud1a0\ucf5c `{protocol}`\uc744(\ub97c) \ud398\uc5b4\ub9c1\ud558\ub294 \ub3d9\uc548 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\ub294 \ubb34\uc2dc\ub429\ub2c8\ub2e4.", + "title": "\uc11c\ube44\uc2a4\ub97c \ucd94\uac00\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "user": { + "data": { + "device_input": "\uae30\uae30" + }, + "description": "\uba3c\uc800 \ucd94\uac00\ud560 Apple TV\uc758 \uae30\uae30 \uc774\ub984(\uc608: \uc8fc\ubc29 \ub610\ub294 \uce68\uc2e4) \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uc7a5\uce58\uac00 \uc790\ub3d9\uc73c\ub85c \ubc1c\uacac\ub41c \uacbd\uc6b0 \ub2e4\uc74c\uacfc \uac19\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.\n\n\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uac70\ub098 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud55c \uacbd\uc6b0 \uae30\uae30 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n{devices}", + "title": "\uc0c8\ub85c\uc6b4 Apple TV \uc124\uc815\ud558\uae30" } } - } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Home Assistant\ub97c \uc2dc\uc791\ud560 \ub54c \uae30\uae30\ub97c \ucf1c\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694" + }, + "description": "\uc77c\ubc18 \uae30\uae30 \uc124\uc815 \uad6c\uc131" + } + } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index d809ac749b7..e313e972188 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -13,29 +13,52 @@ "already_configured": "Apparaat is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", "no_devices_found": "Geen apparaten gevonden op het netwerk", + "no_usable_service": "Er is een apparaat gevonden, maar er kon geen manier worden gevonden om er verbinding mee te maken. Als u dit bericht blijft zien, probeert u het IP-adres in te voeren of uw Apple TV opnieuw op te starten.", "unknown": "Onverwachte fout" }, "flow_title": "Apple TV: {name}", "step": { "confirm": { + "description": "U staat op het punt om de Apple TV met de naam `{name}` toe te voegen aan Home Assistant.\n\n**Om het proces te voltooien, moet u mogelijk meerdere PIN-codes invoeren.**\n\nLet op: u kunt uw Apple TV *niet* uitschakelen met deze integratie. Alleen de mediaspeler in Home Assistant wordt uitgeschakeld!", "title": "Bevestig het toevoegen van Apple TV" }, "pair_no_pin": { + "description": "Koppeling is vereist voor de `{protocol}` service. Voer de PIN {pin} in op uw Apple TV om verder te gaan.", "title": "Koppelen" }, "pair_with_pin": { "data": { "pin": "PIN-code" }, + "description": "Koppelen is vereist voor het `{protocol}` protocol. Voer de PIN-code in die op het scherm wordt getoond. Beginnende nullen moeten worden weggelaten, d.w.z. voer 123 in als de getoonde code 0123 is.", "title": "Koppelen" }, + "reconfigure": { + "description": "Deze Apple TV ondervindt verbindingsproblemen en moet opnieuw worden geconfigureerd.", + "title": "Apparaat herconfiguratie" + }, + "service_problem": { + "description": "Er is een probleem opgetreden tijdens het koppelen van protocol `{protocol}`. Dit wordt genegeerd.", + "title": "Dienst toevoegen mislukt" + }, "user": { "data": { "device_input": "Apparaat" }, + "description": "Begin met het invoeren van de apparaatnaam (bijv. Keuken of Slaapkamer) of het IP-adres van de Apple TV die u wilt toevoegen. Als er automatisch apparaten in uw netwerk zijn gevonden, worden deze hieronder weergegeven.\n\nAls u het apparaat niet kunt zien of problemen ondervindt, probeer dan het IP-adres van het apparaat in te voeren.\n\n{devices}", "title": "Stel een nieuwe Apple TV in" } } }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schakel het apparaat niet in wanneer u Home Assistant start" + }, + "description": "Algemene apparaatinstellingen configureren" + } + } + }, "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 95bf11ddc09..5f4a6b66643 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -42,11 +42,10 @@ def get_service(hass, config, discovery_info=None): _LOGGER.error("Invalid Apprise config url provided") return None - if config.get(CONF_URL): - # Ordered list of URLs - if not a_obj.add(config[CONF_URL]): - _LOGGER.error("Invalid Apprise URL(s) supplied") - return None + # Ordered list of URLs + if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]): + _LOGGER.error("Invalid Apprise URL(s) supplied") + return None return AppriseNotificationService(a_obj) diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 2a8e2a78cac..5a753342b2b 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -2,6 +2,6 @@ "domain": "aqualogic", "name": "AquaLogic", "documentation": "https://www.home-assistant.io/integrations/aqualogic", - "requirements": ["aqualogic==1.0"], + "requirements": ["aqualogic==2.6"], "codeowners": [] } diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index f92cd11c011..315b039f778 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -2,7 +2,7 @@ import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, PERCENTAGE, @@ -12,7 +12,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DOMAIN, UPDATE_TOPIC @@ -56,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class AquaLogicSensor(Entity): +class AquaLogicSensor(SensorEntity): """Sensor implementation for the AquaLogic component.""" def __init__(self, processor, sensor_type): diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 686e7c2de16..fe62c41c061 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -1,5 +1,6 @@ """Arcam component.""" import asyncio +from contextlib import suppress import logging from arcam.fmj import ConnectionFailed @@ -28,10 +29,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def _await_cancel(task): task.cancel() - try: + with suppress(asyncio.CancelledError): await task - except asyncio.CancelledError: - pass async def async_setup(hass: HomeAssistantType, config: ConfigType): diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index c03a082c149..4ae34abb2c2 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Arcam FMJ Receiver control.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Arcam FMJ Receiver control devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -56,7 +56,7 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) + trigger_id = automation_info.get("trigger_id") if automation_info else None job = HassJob(action) if config[CONF_TYPE] == "turn_on": @@ -67,7 +67,13 @@ async def async_attach_trigger( if event.data[ATTR_ENTITY_ID] == entity_id: hass.async_run_hass_job( job, - {"trigger": {**config, "description": f"{DOMAIN} - {entity_id}"}}, + { + "trigger": { + **config, + "description": f"{DOMAIN} - {entity_id}", + "id": trigger_id, + } + }, event.context, ) diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 563ede56155..4af1181a265 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -1,7 +1,17 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/id.json b/homeassistant/components/arcam_fmj/translations/id.json new file mode 100644 index 00000000000..96b10140948 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Arcam FMJ di {host}", + "step": { + "confirm": { + "description": "Ingin menambahkan Arcam FMJ `{host}` ke Home Assistant?" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Masukkan nama host atau alamat IP perangkat." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} diminta untuk dinyalakan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ko.json b/homeassistant/components/arcam_fmj/translations/ko.json index 532e5ef4c5f..29a2887f7e2 100644 --- a/homeassistant/components/arcam_fmj/translations/ko.json +++ b/homeassistant/components/arcam_fmj/translations/ko.json @@ -8,7 +8,7 @@ "flow_title": "Arcam FMJ: {host}", "step": { "confirm": { - "description": "Home Assistant \uc5d0 Arcam FMJ `{host}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "Home Assistant\uc5d0 Arcam FMJ `{host}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { @@ -21,7 +21,7 @@ }, "device_automation": { "trigger_type": { - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" + "turn_on": "{entity_name}\uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/pt.json b/homeassistant/components/arcam_fmj/translations/pt.json index 097e3d086d6..af72dfe96e2 100644 --- a/homeassistant/components/arcam_fmj/translations/pt.json +++ b/homeassistant/components/arcam_fmj/translations/pt.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index f31272b3a20..588a652660a 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -1,10 +1,9 @@ """Support for getting information from Arduino pins.""" import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DOMAIN @@ -30,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class ArduinoSensor(Entity): +class ArduinoSensor(SensorEntity): """Representation of an Arduino Sensor.""" def __init__(self, name, pin, pin_type, board): diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index d213a3d2903..061c15eafb0 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -124,7 +123,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class ArestSensor(Entity): +class ArestSensor(SensorEntity): """Implementation of an aREST sensor for exposed variables.""" def __init__( diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 47328d5cbc2..dd899cbd04f 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -136,7 +136,7 @@ class ArloBaseStation(AlarmControlPanelEntity): return self._base_station.name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 36e32181702..c1848661429 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -108,7 +108,7 @@ class ArloCam(Camera): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { name: value diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index d5d583de22c..c794bf1ef5e 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONCENTRATION_PARTS_PER_MILLION, @@ -16,7 +16,6 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from . import ATTRIBUTION, DATA_ARLO, DEFAULT_BRAND, SIGNAL_UPDATE_ARLO @@ -73,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class ArloSensor(Entity): +class ArloSensor(SensorEntity): """An implementation of a Netgear Arlo IP sensor.""" def __init__(self, name, device, sensor_type): @@ -183,7 +182,7 @@ class ArloSensor(Entity): self._state = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attrs = {} diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index e63bef9c108..1011d76f8aa 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -1,5 +1,5 @@ """Support for Arris TG2492LG router.""" -from typing import List +from __future__ import annotations from arris_tg2492lg import ConnectBox, Device import voluptuous as vol @@ -36,7 +36,7 @@ class ArrisDeviceScanner(DeviceScanner): def __init__(self, connect_box: ConnectBox): """Initialize the scanner.""" self.connect_box = connect_box - self.last_results: List[Device] = [] + self.last_results: list[Device] = [] def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 18186e8b871..ba9166d1af5 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -3,9 +3,9 @@ import json import logging from homeassistant.components import mqtt +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEGREE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -114,7 +114,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return True -class ArwnSensor(Entity): +class ArwnSensor(SensorEntity): """Representation of an ARWN sensor.""" def __init__(self, topic, name, state_key, units, icon=None): @@ -154,7 +154,7 @@ class ArwnSensor(Entity): return self._uid @property - def state_attributes(self): + def extra_state_attributes(self): """Return all the state attributes.""" return self.event diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 28e8fe76684..25a78f6a523 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -31,7 +31,6 @@ from .const import ( MODE_ROUTER, PROTOCOL_SSH, PROTOCOL_TELNET, - SENSOR_TYPES, ) from .router import AsusWrtRouter @@ -39,6 +38,7 @@ PLATFORMS = ["device_tracker", "sensor"] CONF_PUB_KEY = "pub_key" SECRET_GROUP = "Password or SSH Key" +SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"] CONFIG_SCHEMA = vol.Schema( vol.All( diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index b3e3ec4d68d..af778dbe972 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -21,7 +21,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -# pylint:disable=unused-import from .const import ( CONF_DNSMASQ, CONF_INTERFACE, diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index 40752e81a08..a8977a77ea8 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -20,5 +20,9 @@ MODE_ROUTER = "router" PROTOCOL_SSH = "ssh" PROTOCOL_TELNET = "telnet" -# Sensor -SENSOR_TYPES = ["devices", "upload_speed", "download_speed", "download", "upload"] +# Sensors +SENSOR_CONNECTED_DEVICE = "sensor_connected_device" +SENSOR_RX_BYTES = "sensor_rx_bytes" +SENSOR_TX_BYTES = "sensor_tx_bytes" +SENSOR_RX_RATES = "sensor_rx_rates" +SENSOR_TX_RATES = "sensor_tx_rates" diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index 385b25755b0..bd86dd21edd 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,5 +1,5 @@ """Support for ASUSWRT routers.""" -from typing import Dict +from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -56,41 +56,22 @@ class AsusWrtDevice(ScannerEntity): def __init__(self, router: AsusWrtRouter, device) -> None: """Initialize a AsusWrt device.""" self._router = router - self._mac = device.mac - self._name = device.name or DEFAULT_DEVICE_NAME - self._active = False - self._icon = None - self._attrs = {} - - @callback - def async_update_state(self) -> None: - """Update the AsusWrt device.""" - device = self._router.devices[self._mac] - self._active = device.is_connected - - self._attrs = { - "mac": device.mac, - "ip_address": device.ip_address, - } - if device.last_activity: - self._attrs["last_time_reachable"] = device.last_activity.isoformat( - timespec="seconds" - ) + self._device = device @property def unique_id(self) -> str: """Return a unique ID.""" - return self._mac + return self._device.mac @property def name(self) -> str: """Return the name.""" - return self._name + return self._device.name or DEFAULT_DEVICE_NAME @property def is_connected(self): """Return true if the device is connected to the network.""" - return self._active + return self._device.is_connected @property def source_type(self) -> str: @@ -98,24 +79,28 @@ class AsusWrtDevice(ScannerEntity): return SOURCE_TYPE_ROUTER @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def device_state_attributes(self) -> Dict[str, any]: + def extra_state_attributes(self) -> dict[str, any]: """Return the attributes.""" - return self._attrs + attrs = { + "mac": self._device.mac, + "ip_address": self._device.ip_address, + } + if self._device.last_activity: + attrs["last_time_reachable"] = self._device.last_activity.isoformat( + timespec="seconds" + ) + return 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)}, - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "AsusWRT Tracked device", + data = { + "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, } + if self._device.name: + data["default_name"] = self._device.name + + return data @property def should_poll(self) -> bool: @@ -125,12 +110,11 @@ class AsusWrtDevice(ScannerEntity): @callback def async_on_demand_update(self): """Update state.""" - self.async_update_state() + self._device = self._router.devices[self._device.mac] self.async_write_ha_state() async def async_added_to_hass(self): """Register state update callback.""" - self.async_update_state() self.async_on_remove( async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 4af157387f9..c5880ea11bb 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -1,7 +1,9 @@ """Represent the AsusWrt router.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging -from typing import Any, Dict, Optional +from typing import Any from aioasuswrt.asuswrt import AsusWrt @@ -24,6 +26,7 @@ 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 from .const import ( @@ -37,14 +40,97 @@ from .const import ( DEFAULT_TRACK_UNKNOWN, DOMAIN, PROTOCOL_TELNET, + SENSOR_CONNECTED_DEVICE, + SENSOR_RX_BYTES, + SENSOR_RX_RATES, + SENSOR_TX_BYTES, + SENSOR_TX_RATES, ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] + +KEY_COORDINATOR = "coordinator" +KEY_SENSORS = "sensors" + SCAN_INTERVAL = timedelta(seconds=30) +SENSORS_TYPE_BYTES = "sensors_bytes" +SENSORS_TYPE_COUNT = "sensors_count" +SENSORS_TYPE_RATES = "sensors_rates" + _LOGGER = logging.getLogger(__name__) +class AsusWrtSensorDataHandler: + """Data handler for AsusWrt sensor.""" + + def __init__(self, hass, api): + """Initialize a AsusWrt sensor data handler.""" + self._hass = hass + self._api = api + self._connected_devices = 0 + + async def _get_connected_devices(self): + """Return number of connected devices.""" + return {SENSOR_CONNECTED_DEVICE: self._connected_devices} + + async def _get_bytes(self): + """Fetch byte information from the router.""" + ret_dict: dict[str, Any] = {} + try: + datas = await self._api.async_get_bytes_total() + except OSError as exc: + raise UpdateFailed from exc + + ret_dict[SENSOR_RX_BYTES] = datas[0] + ret_dict[SENSOR_TX_BYTES] = datas[1] + + return ret_dict + + async def _get_rates(self): + """Fetch rates information from the router.""" + ret_dict: dict[str, Any] = {} + try: + rates = await self._api.async_get_current_transfer_rates() + except OSError as exc: + raise UpdateFailed from exc + + ret_dict[SENSOR_RX_RATES] = rates[0] + ret_dict[SENSOR_TX_RATES] = rates[1] + + return ret_dict + + def update_device_count(self, conn_devices: int): + """Update connected devices attribute.""" + if self._connected_devices == conn_devices: + return False + self._connected_devices = conn_devices + return True + + async def get_coordinator(self, sensor_type: str, should_poll=True): + """Get the coordinator for a specific sensor type.""" + if sensor_type == SENSORS_TYPE_COUNT: + method = self._get_connected_devices + elif sensor_type == SENSORS_TYPE_BYTES: + method = self._get_bytes + elif sensor_type == SENSORS_TYPE_RATES: + method = self._get_rates + else: + raise RuntimeError(f"Invalid sensor type: {sensor_type}") + + coordinator = DataUpdateCoordinator( + self._hass, + _LOGGER, + name=sensor_type, + update_method=method, + # Polling interval. Will only be polled if there are subscribers. + update_interval=SCAN_INTERVAL if should_poll else None, + ) + await coordinator.async_refresh() + + return coordinator + + class AsusWrtDevInfo: """Representation of a AsusWrt device info.""" @@ -110,9 +196,13 @@ class AsusWrtRouter: self._protocol = entry.data[CONF_PROTOCOL] self._host = entry.data[CONF_HOST] - self._devices: Dict[str, Any] = {} + self._devices: dict[str, Any] = {} + self._connected_devices = 0 self._connect_error = False + self._sensors_data_handler: AsusWrtSensorDataHandler = None + self._sensors_coordinator: dict[str, Any] = {} + self._on_close = [] self._options = { @@ -150,11 +240,14 @@ class AsusWrtRouter: # Update devices await self.update_devices() + # Init Sensors + await self.init_sensors_coordinator() + self.async_on_close( async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) ) - async def update_all(self, now: Optional[datetime] = None) -> None: + async def update_all(self, now: datetime | None = None) -> None: """Update all AsusWrt platforms.""" await self.update_devices() @@ -201,11 +294,55 @@ class AsusWrtRouter: if new_device: async_dispatcher_send(self.hass, self.signal_device_new) + self._connected_devices = len(wrt_devices) + await self._update_unpolled_sensors() + + async def init_sensors_coordinator(self) -> None: + """Init AsusWrt sensors coordinators.""" + if self._sensors_data_handler: + return + + self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api) + self._sensors_data_handler.update_device_count(self._connected_devices) + + conn_dev_coordinator = await self._sensors_data_handler.get_coordinator( + SENSORS_TYPE_COUNT, False + ) + self._sensors_coordinator[SENSORS_TYPE_COUNT] = { + KEY_COORDINATOR: conn_dev_coordinator, + KEY_SENSORS: [SENSOR_CONNECTED_DEVICE], + } + + bytes_coordinator = await self._sensors_data_handler.get_coordinator( + SENSORS_TYPE_BYTES + ) + self._sensors_coordinator[SENSORS_TYPE_BYTES] = { + KEY_COORDINATOR: bytes_coordinator, + KEY_SENSORS: [SENSOR_RX_BYTES, SENSOR_TX_BYTES], + } + + rates_coordinator = await self._sensors_data_handler.get_coordinator( + SENSORS_TYPE_RATES + ) + self._sensors_coordinator[SENSORS_TYPE_RATES] = { + KEY_COORDINATOR: rates_coordinator, + KEY_SENSORS: [SENSOR_RX_RATES, SENSOR_TX_RATES], + } + + async def _update_unpolled_sensors(self) -> None: + """Request refresh for AsusWrt unpolled sensors.""" + if not self._sensors_data_handler: + return + + if SENSORS_TYPE_COUNT in self._sensors_coordinator: + coordinator = self._sensors_coordinator[SENSORS_TYPE_COUNT][KEY_COORDINATOR] + if self._sensors_data_handler.update_device_count(self._connected_devices): + await coordinator.async_refresh() + async def close(self) -> None: """Close the connection.""" - if self._api is not None: - if self._protocol == PROTOCOL_TELNET: - self._api.connection.disconnect() + if self._api is not None and self._protocol == PROTOCOL_TELNET: + self._api.connection.disconnect() self._api = None for func in self._on_close: @@ -217,7 +354,7 @@ class AsusWrtRouter: """Add a function to call when router is closed.""" self._on_close.append(func) - def update_options(self, new_options: Dict) -> bool: + def update_options(self, new_options: dict) -> bool: """Update router options.""" req_reload = False for name, new_opt in new_options.items(): @@ -230,6 +367,16 @@ class AsusWrtRouter: self._options.update(new_options) return req_reload + @property + def device_info(self) -> dict[str, Any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, "AsusWRT")}, + "name": self._host, + "model": "Asus Router", + "manufacturer": "Asus", + } + @property def signal_device_new(self) -> str: """Event specific per AsusWrt entry to signal new device.""" @@ -246,17 +393,22 @@ class AsusWrtRouter: return self._host @property - def devices(self) -> Dict[str, Any]: + def devices(self) -> dict[str, Any]: """Return devices.""" return self._devices + @property + def sensors_coordinator(self) -> dict[str, Any]: + """Return sensors coordinators.""" + return self._sensors_coordinator + @property def api(self) -> AsusWrt: """Return router API.""" return self._api -def get_api(conf: Dict, options: Optional[Dict] = None) -> AsusWrt: +def get_api(conf: dict, options: dict | None = None) -> AsusWrt: """Get the AsusWrt API.""" opt = options or {} diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 2a39d339f06..7e38243e3d6 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,236 +1,171 @@ """Asuswrt status sensors.""" -from datetime import timedelta -import enum +from __future__ import annotations + import logging -from typing import Any, Dict, List, Optional - -from aioasuswrt.asuswrt import AsusWrt +from numbers import Number +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND +from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import DATA_ASUSWRT, DOMAIN, SENSOR_TYPES +from .const import ( + DATA_ASUSWRT, + DOMAIN, + SENSOR_CONNECTED_DEVICE, + SENSOR_RX_BYTES, + SENSOR_RX_RATES, + SENSOR_TX_BYTES, + SENSOR_TX_RATES, +) +from .router import KEY_COORDINATOR, KEY_SENSORS, AsusWrtRouter -UPLOAD_ICON = "mdi:upload-network" -DOWNLOAD_ICON = "mdi:download-network" +DEFAULT_PREFIX = "Asuswrt" + +SENSOR_DEVICE_CLASS = "device_class" +SENSOR_ICON = "icon" +SENSOR_NAME = "name" +SENSOR_UNIT = "unit" +SENSOR_FACTOR = "factor" +SENSOR_DEFAULT_ENABLED = "default_enabled" + +UNIT_DEVICES = "Devices" + +CONNECTION_SENSORS = { + SENSOR_CONNECTED_DEVICE: { + SENSOR_NAME: "Devices Connected", + SENSOR_UNIT: UNIT_DEVICES, + SENSOR_FACTOR: 0, + SENSOR_ICON: "mdi:router-network", + SENSOR_DEVICE_CLASS: None, + SENSOR_DEFAULT_ENABLED: True, + }, + SENSOR_RX_RATES: { + SENSOR_NAME: "Download Speed", + SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, + SENSOR_FACTOR: 125000, + SENSOR_ICON: "mdi:download-network", + SENSOR_DEVICE_CLASS: None, + }, + SENSOR_TX_RATES: { + SENSOR_NAME: "Upload Speed", + SENSOR_UNIT: DATA_RATE_MEGABITS_PER_SECOND, + SENSOR_FACTOR: 125000, + SENSOR_ICON: "mdi:upload-network", + SENSOR_DEVICE_CLASS: None, + }, + SENSOR_RX_BYTES: { + SENSOR_NAME: "Download", + SENSOR_UNIT: DATA_GIGABYTES, + SENSOR_FACTOR: 1000000000, + SENSOR_ICON: "mdi:download", + SENSOR_DEVICE_CLASS: None, + }, + SENSOR_TX_BYTES: { + SENSOR_NAME: "Upload", + SENSOR_UNIT: DATA_GIGABYTES, + SENSOR_FACTOR: 1000000000, + SENSOR_ICON: "mdi:upload", + SENSOR_DEVICE_CLASS: None, + }, +} _LOGGER = logging.getLogger(__name__) -@enum.unique -class _SensorTypes(enum.Enum): - DEVICES = "devices" - UPLOAD = "upload" - DOWNLOAD = "download" - DOWNLOAD_SPEED = "download_speed" - UPLOAD_SPEED = "upload_speed" - - @property - def unit_of_measurement(self) -> Optional[str]: - """Return a string with the unit of the sensortype.""" - if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD): - return DATA_GIGABYTES - if self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED): - return DATA_RATE_MEGABITS_PER_SECOND - if self == _SensorTypes.DEVICES: - return "devices" - return None - - @property - def icon(self) -> Optional[str]: - """Return the expected icon for the sensortype.""" - if self in (_SensorTypes.UPLOAD, _SensorTypes.UPLOAD_SPEED): - return UPLOAD_ICON - if self in (_SensorTypes.DOWNLOAD, _SensorTypes.DOWNLOAD_SPEED): - return DOWNLOAD_ICON - return None - - @property - def sensor_name(self) -> Optional[str]: - """Return the name of the sensor.""" - if self is _SensorTypes.DEVICES: - return "Asuswrt Devices Connected" - if self is _SensorTypes.UPLOAD: - return "Asuswrt Upload" - if self is _SensorTypes.DOWNLOAD: - return "Asuswrt Download" - if self is _SensorTypes.UPLOAD_SPEED: - return "Asuswrt Upload Speed" - if self is _SensorTypes.DOWNLOAD_SPEED: - return "Asuswrt Download Speed" - return None - - @property - def is_speed(self) -> bool: - """Return True if the type is an upload/download speed.""" - return self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED) - - @property - def is_size(self) -> bool: - """Return True if the type is the total upload/download size.""" - return self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD) - - -class _SensorInfo: - """Class handling sensor information.""" - - def __init__(self, sensor_type: _SensorTypes): - """Initialize the handler class.""" - self.type = sensor_type - self.enabled = False - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: - """Set up the asuswrt sensors.""" + """Set up the sensors.""" + router: AsusWrtRouter = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] + entities = [] - router = hass.data[DOMAIN][entry.entry_id][DATA_ASUSWRT] - api: AsusWrt = router.api - device_name = entry.data.get(CONF_NAME, "AsusWRT") + for sensor_data in router.sensors_coordinator.values(): + coordinator = sensor_data[KEY_COORDINATOR] + sensors = sensor_data[KEY_SENSORS] + for sensor_key in sensors: + if sensor_key in CONNECTION_SENSORS: + entities.append( + AsusWrtSensor( + coordinator, router, sensor_key, CONNECTION_SENSORS[sensor_key] + ) + ) - # Let's discover the valid sensor types. - sensors = [_SensorInfo(_SensorTypes(x)) for x in SENSOR_TYPES] - - data_handler = AsuswrtDataHandler(sensors, api) - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="sensor", - update_method=data_handler.update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=30), - ) - - await coordinator.async_refresh() - async_add_entities( - [AsuswrtSensor(coordinator, data_handler, device_name, x.type) for x in sensors] - ) + async_add_entities(entities, True) -class AsuswrtDataHandler: - """Class handling the API updates.""" - - def __init__(self, sensors: List[_SensorInfo], api: AsusWrt): - """Initialize the handler class.""" - self._api = api - self._sensors = sensors - self._connected = True - - def enable_sensor(self, sensor_type: _SensorTypes): - """Enable a specific sensor type.""" - for index, sensor in enumerate(self._sensors): - if sensor.type == sensor_type: - self._sensors[index].enabled = True - return - - def disable_sensor(self, sensor_type: _SensorTypes): - """Disable a specific sensor type.""" - for index, sensor in enumerate(self._sensors): - if sensor.type == sensor_type: - self._sensors[index].enabled = False - return - - async def update_data(self) -> Dict[_SensorTypes, Any]: - """Fetch the relevant data from the router.""" - ret_dict: Dict[_SensorTypes, Any] = {} - try: - if _SensorTypes.DEVICES in [x.type for x in self._sensors if x.enabled]: - # Let's check the nr of devices. - devices = await self._api.async_get_connected_devices() - ret_dict[_SensorTypes.DEVICES] = len(devices) - - if any(x.type.is_speed for x in self._sensors if x.enabled): - # Let's check the upload and download speed - speed = await self._api.async_get_current_transfer_rates() - ret_dict[_SensorTypes.DOWNLOAD_SPEED] = round(speed[0] / 125000, 2) - ret_dict[_SensorTypes.UPLOAD_SPEED] = round(speed[1] / 125000, 2) - - if any(x.type.is_size for x in self._sensors if x.enabled): - rates = await self._api.async_get_bytes_total() - ret_dict[_SensorTypes.DOWNLOAD] = round(rates[0] / 1000000000, 1) - ret_dict[_SensorTypes.UPLOAD] = round(rates[1] / 1000000000, 1) - - if not self._connected: - # Log a successful reconnect - self._connected = True - _LOGGER.warning("Successfully reconnected to ASUS router") - - except OSError as err: - if self._connected: - # Log the first time connection was lost - _LOGGER.warning("Lost connection to router error due to: '%s'", err) - self._connected = False - - return ret_dict - - -class AsuswrtSensor(CoordinatorEntity): - """The asuswrt specific sensor class.""" +class AsusWrtSensor(CoordinatorEntity, SensorEntity): + """Representation of a AsusWrt sensor.""" def __init__( self, coordinator: DataUpdateCoordinator, - data_handler: AsuswrtDataHandler, - device_name: str, - sensor_type: _SensorTypes, - ): - """Initialize the sensor class.""" + router: AsusWrtRouter, + sensor_type: str, + sensor: dict[str, any], + ) -> None: + """Initialize a AsusWrt sensor.""" super().__init__(coordinator) - self._handler = data_handler - self._device_name = device_name - self._type = sensor_type - - @property - def state(self): - """Return the state of the sensor.""" - return self.coordinator.data.get(self._type) - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._type.sensor_name - - @property - def icon(self) -> Optional[str]: - """Return the icon to use in the frontend.""" - return self._type.icon - - @property - def unit_of_measurement(self) -> Optional[str]: - """Return the unit.""" - return self._type.unit_of_measurement - - @property - def unique_id(self) -> str: - """Return the unique_id of the sensor.""" - return f"{DOMAIN} {self._type.sensor_name}" - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "identifiers": {(DOMAIN, "AsusWRT")}, - "name": self._device_name, - "model": "Asus Router", - "manufacturer": "Asus", - } + self._router = router + self._sensor_type = sensor_type + self._name = f"{DEFAULT_PREFIX} {sensor[SENSOR_NAME]}" + self._unique_id = f"{DOMAIN} {self._name}" + self._unit = sensor[SENSOR_UNIT] + self._factor = sensor[SENSOR_FACTOR] + self._icon = sensor[SENSOR_ICON] + self._device_class = sensor[SENSOR_DEVICE_CLASS] + self._default_enabled = sensor.get(SENSOR_DEFAULT_ENABLED, False) @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return False + return self._default_enabled - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - self._handler.enable_sensor(self._type) - await super().async_added_to_hass() + @property + def state(self) -> str: + """Return current state.""" + state = self.coordinator.data.get(self._sensor_type) + if state is None: + return None + if self._factor and isinstance(state, Number): + return round(state / self._factor, 2) + return state - async def async_will_remove_from_hass(self): - """Call when entity is removed from hass.""" - self._handler.disable_sensor(self._type) + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def unit_of_measurement(self) -> str: + """Return the unit.""" + return self._unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def device_class(self) -> str: + """Return the device_class.""" + return self._device_class + + @property + def extra_state_attributes(self) -> dict[str, any]: + """Return the attributes.""" + return {"hostname": self._router.host} + + @property + def device_info(self) -> dict[str, any]: + """Return the device information.""" + return self._router.device_info diff --git a/homeassistant/components/asuswrt/translations/bg.json b/homeassistant/components/asuswrt/translations/bg.json new file mode 100644 index 00000000000..dbb5f415f92 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index 433bf17b814..36699d95753 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -6,6 +6,9 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "pwd_and_ssh": "Nur Passwort oder SSH-Schl\u00fcsseldatei angeben", + "pwd_or_ssh": "Bitte Passwort oder SSH-Schl\u00fcsseldatei angeben", + "ssh_not_file": "SSH-Schl\u00fcsseldatei nicht gefunden", "unknown": "Unerwarteter Fehler" }, "step": { @@ -16,8 +19,22 @@ "name": "Name", "password": "Passwort", "port": "Port", + "protocol": "Zu verwendendes Kommunikationsprotokoll", + "ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)", "username": "Benutzername" - } + }, + "title": "" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "interface": "Schnittstelle, von der du Statistiken haben m\u00f6chtest (z.B. eth0, eth1 usw.)", + "require_ip": "Ger\u00e4te m\u00fcssen IP haben (f\u00fcr Zugangspunkt-Modus)" + }, + "title": "AsusWRT Optionen" } } } diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json new file mode 100644 index 00000000000..4f47781a15c --- /dev/null +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "mode": "M\u00f3d", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "title": "AsusWRT Be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/id.json b/homeassistant/components/asuswrt/translations/id.json new file mode 100644 index 00000000000..aa4eebd1f86 --- /dev/null +++ b/homeassistant/components/asuswrt/translations/id.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "pwd_and_ssh": "Hanya berikan kata sandi atau file kunci SSH", + "pwd_or_ssh": "Harap berikan kata sandi atau file kunci SSH", + "ssh_not_file": "File kunci SSH tidak ditemukan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "mode": "Mode", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "protocol": "Protokol komunikasi yang akan digunakan", + "ssh_key": "Jalur ke file kunci SSH Anda (bukan kata sandi)", + "username": "Nama Pengguna" + }, + "description": "Tetapkan parameter yang diperlukan untuk terhubung ke router Anda", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Tenggang waktu dalam detik untuk perangkat dianggap sebagai keluar", + "dnsmasq": "Lokasi dnsmasq.leases di router file", + "interface": "Antarmuka statistik yang diinginkan (mis. eth0, eth1, dll)", + "require_ip": "Perangkat harus memiliki IP (untuk mode titik akses)", + "track_unknown": "Lacak perangkat yang tidak dikenal/tidak bernama" + }, + "title": "Opsi AsusWRT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ko.json b/homeassistant/components/asuswrt/translations/ko.json index de3de06e6b1..4e60a0d15b0 100644 --- a/homeassistant/components/asuswrt/translations/ko.json +++ b/homeassistant/components/asuswrt/translations/ko.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \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", + "pwd_and_ssh": "\ube44\ubc00\ubc88\ud638 \ub610\ub294 SSH \ud0a4 \ud30c\uc77c\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4", + "pwd_or_ssh": "\ube44\ubc00\ubc88\ud638 \ub610\ub294 SSH \ud0a4 \ud30c\uc77c\uc744 \ub123\uc5b4\uc8fc\uc138\uc694", + "ssh_not_file": "SSH \ud0a4 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { @@ -16,8 +19,26 @@ "name": "\uc774\ub984", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", + "protocol": "\uc0ac\uc6a9\ud560 \ud1b5\uc2e0 \ud504\ub85c\ud1a0\ucf5c", + "ssh_key": "SSH \ud0a4 \ud30c\uc77c\uc758 \uacbd\ub85c (\ube44\ubc00\ubc88\ud638 \ub300\uccb4)", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "description": "\ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub370 \ud544\uc694\ud55c \ub9e4\uac1c \ubcc0\uc218\ub97c \uc124\uc815\ud558\uae30", + "title": "AsusWRT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\uae30\uae30\uac00 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\uae30 \uc804\uc5d0 \uae30\ub2e4\ub9ac\ub294 \uc2dc\uac04 (\ucd08)", + "dnsmasq": "dnsmasq.lease \ud30c\uc77c\uc758 \ub77c\uc6b0\ud130 \uc704\uce58", + "interface": "\ud1b5\uacc4\ub97c \uc6d0\ud558\ub294 \uc778\ud130\ud398\uc774\uc2a4 (\uc608: eth0, eth1 \ub4f1)", + "require_ip": "\uae30\uae30\uc5d0\ub294 IP\uac00 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4 (\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \ubaa8\ub4dc\uc778 \uacbd\uc6b0)", + "track_unknown": "\uc54c \uc218 \uc5c6\uac70\ub098 \uc774\ub984\uc774 \uc5c6\ub294 \uae30\uae30 \ucd94\uc801\ud558\uae30" + }, + "title": "AsusWRT \uc635\uc158" } } } diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index 9d1e76aaf2b..f6f347f771f 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", + "pwd_and_ssh": "Geef alleen wachtwoord of SSH-sleutelbestand op", + "pwd_or_ssh": "Geef een wachtwoord of SSH-sleutelbestand op", "ssh_not_file": "SSH-sleutelbestand niet gevonden", "unknown": "Onverwachte fout" }, @@ -17,8 +19,11 @@ "name": "Naam", "password": "Wachtwoord", "port": "Poort", + "protocol": "Te gebruiken communicatieprotocol", + "ssh_key": "Pad naar uw SSH-sleutelbestand (in plaats van wachtwoord)", "username": "Gebruikersnaam" }, + "description": "Stel de vereiste parameter in om verbinding te maken met uw router", "title": "AsusWRT" } } @@ -27,6 +32,10 @@ "step": { "init": { "data": { + "consider_home": "Aantal seconden dat wordt gewacht voordat een apparaat als afwezig wordt beschouwd", + "dnsmasq": "De locatie in de router van de dnsmasq.leases-bestanden", + "interface": "De interface waarvan u statistieken wilt (bijv. Eth0, eth1 enz.)", + "require_ip": "Apparaten moeten een IP-adres hebben (voor toegangspuntmodus)", "track_unknown": "Volg onbekende / naamloze apparaten" }, "title": "AsusWRT-opties" diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index 236f7642c12..a2090b1faf6 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -21,7 +21,7 @@ "port": "\u041f\u043e\u0440\u0442", "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0441\u0432\u044f\u0437\u0438", "ssh_key": "\u041f\u0443\u0442\u044c \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0443.", "title": "AsusWRT" diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 7489ada3341..017e9968d1e 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -10,7 +10,6 @@ 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.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -24,24 +23,34 @@ DOMAIN = "atag" PLATFORMS = [CLIMATE, WATER_HEATER, SENSOR] -async def async_setup(hass: HomeAssistant, config): - """Set up the Atag component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Atag integration from a config entry.""" - session = async_get_clientsession(hass) - coordinator = AtagDataUpdateCoordinator(hass, session, entry) - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + async def _async_update_data(): + """Update data via library.""" + with async_timeout.timeout(20): + try: + await atag.update() + except AtagException as err: + raise UpdateFailed(err) from err + return atag - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + atag = AtagOne( + session=async_get_clientsession(hass), **entry.data, device=entry.unique_id + ) + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN.title(), + update_method=_async_update_data, + update_interval=timedelta(seconds=60), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator if entry.unique_id is None: - hass.config_entries.async_update_entry(entry, unique_id=coordinator.atag.id) + hass.config_entries.async_update_entry(entry, unique_id=atag.id) for platform in PLATFORMS: hass.async_create_task( @@ -51,35 +60,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -class AtagDataUpdateCoordinator(DataUpdateCoordinator): - """Define an object to hold Atag data.""" - - def __init__(self, hass, session, entry): - """Initialize.""" - self.atag = AtagOne(session=session, **entry.data) - - super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) - ) - - async def _async_update_data(self): - """Update data via library.""" - with async_timeout.timeout(20): - try: - if not await self.atag.update(): - raise UpdateFailed("No data received") - except AtagException as error: - raise UpdateFailed(error) from error - return self.atag.report - - 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, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -91,7 +78,7 @@ async def async_unload_entry(hass, entry): class AtagEntity(CoordinatorEntity): """Defines a base Atag entity.""" - def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None: + def __init__(self, coordinator: DataUpdateCoordinator, atag_id: str) -> None: """Initialize the Atag entity.""" super().__init__(coordinator) @@ -101,8 +88,8 @@ class AtagEntity(CoordinatorEntity): @property def device_info(self) -> dict: """Return info for device registry.""" - device = self.coordinator.atag.id - version = self.coordinator.atag.apiversion + device = self.coordinator.data.id + version = self.coordinator.data.apiversion return { "identifiers": {(DOMAIN, device)}, "name": "Atag Thermostat", @@ -119,4 +106,4 @@ class AtagEntity(CoordinatorEntity): @property def unique_id(self): """Return a unique ID to use for this entity.""" - return f"{self.coordinator.atag.id}-{self._id}" + return f"{self.coordinator.data.id}-{self._id}" diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index ad46fefe8c2..da7e6a14a73 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -1,5 +1,5 @@ """Initialization of ATAG One climate platform.""" -from typing import List, Optional +from __future__ import annotations from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -16,16 +16,14 @@ from homeassistant.const import ATTR_TEMPERATURE from . import CLIMATE, DOMAIN, AtagEntity -PRESET_SCHEDULE = "Auto" -PRESET_MANUAL = "Manual" -PRESET_EXTEND = "Extend" -SUPPORT_PRESET = [ - PRESET_MANUAL, - PRESET_SCHEDULE, - PRESET_EXTEND, - PRESET_AWAY, - PRESET_BOOST, -] +PRESET_MAP = { + "Manual": "manual", + "Auto": "automatic", + "Extend": "extend", + PRESET_AWAY: "vacation", + PRESET_BOOST: "fireplace", +} +PRESET_INVERTED = {v: k for k, v in PRESET_MAP.items()} SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] @@ -45,60 +43,60 @@ class AtagThermostat(AtagEntity, ClimateEntity): return SUPPORT_FLAGS @property - def hvac_mode(self) -> Optional[str]: + def hvac_mode(self) -> str | None: """Return hvac operation ie. heat, cool mode.""" - if self.coordinator.atag.climate.hvac_mode in HVAC_MODES: - return self.coordinator.atag.climate.hvac_mode + if self.coordinator.data.climate.hvac_mode in HVAC_MODES: + return self.coordinator.data.climate.hvac_mode return None @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return HVAC_MODES @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation.""" - if self.coordinator.atag.climate.status: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE + is_active = self.coordinator.data.climate.status + return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE @property - def temperature_unit(self): + def temperature_unit(self) -> str | None: """Return the unit of measurement.""" - return self.coordinator.atag.climate.temp_unit + return self.coordinator.data.climate.temp_unit @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" - return self.coordinator.atag.climate.temperature + return self.coordinator.data.climate.temperature @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return self.coordinator.atag.climate.target_temperature + return self.coordinator.data.climate.target_temperature @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., auto, manual, fireplace, extend, etc.""" - return self.coordinator.atag.climate.preset_mode + preset = self.coordinator.data.climate.preset_mode + return PRESET_INVERTED.get(preset) @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" - return SUPPORT_PRESET + return list(PRESET_MAP.keys()) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - await self.coordinator.atag.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) + await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.coordinator.atag.climate.set_hvac_mode(hvac_mode) + await self.coordinator.data.climate.set_hvac_mode(hvac_mode) self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self.coordinator.atag.climate.set_preset_mode(preset_mode) + await self.coordinator.data.climate.set_preset_mode(PRESET_MAP[preset_mode]) self.async_write_ha_state() diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index 865159aa658..20055bd8a9a 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -3,14 +3,13 @@ import pyatag import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import DOMAIN # pylint: disable=unused-import +from . import DOMAIN DATA_SCHEMA = { vol.Required(CONF_HOST): str, - vol.Optional(CONF_EMAIL): str, vol.Required(CONF_PORT, default=pyatag.const.DEFAULT_PORT): vol.Coerce(int), } @@ -26,15 +25,14 @@ class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return await self._show_form() - session = async_get_clientsession(self.hass) - try: - atag = pyatag.AtagOne(session=session, **user_input) - await atag.authorize() - await atag.update(force=True) - except pyatag.errors.Unauthorized: + atag = pyatag.AtagOne(session=async_get_clientsession(self.hass), **user_input) + try: + await atag.update() + + except pyatag.Unauthorized: return await self._show_form({"base": "unauthorized"}) - except pyatag.errors.AtagException: + except pyatag.AtagException: return await self._show_form({"base": "cannot_connect"}) await self.async_set_unique_id(atag.id) diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 5e94afb06d3..1154a120f91 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,6 +3,6 @@ "name": "Atag", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", - "requirements": ["pyatag==0.3.4.4"], + "requirements": ["pyatag==0.3.5.3"], "codeowners": ["@MatsNL"] } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index d6abe16ffdb..88ccbdc899f 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -1,4 +1,5 @@ """Initialization of ATAG One sensor platform.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -26,13 +27,10 @@ SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Initialize sensor platform from config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - entities = [] - for sensor in SENSORS: - entities.append(AtagSensor(coordinator, sensor)) - async_add_entities(entities) + async_add_entities([AtagSensor(coordinator, sensor) for sensor in SENSORS]) -class AtagSensor(AtagEntity): +class AtagSensor(AtagEntity, SensorEntity): """Representation of a AtagOne Sensor.""" def __init__(self, coordinator, sensor): @@ -43,32 +41,32 @@ class AtagSensor(AtagEntity): @property def state(self): """Return the state of the sensor.""" - return self.coordinator.data[self._id].state + return self.coordinator.data.report[self._id].state @property def icon(self): """Return icon.""" - return self.coordinator.data[self._id].icon + return self.coordinator.data.report[self._id].icon @property def device_class(self): """Return deviceclass.""" - if self.coordinator.data[self._id].sensorclass in [ + if self.coordinator.data.report[self._id].sensorclass in [ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, ]: - return self.coordinator.data[self._id].sensorclass + return self.coordinator.data.report[self._id].sensorclass return None @property def unit_of_measurement(self): """Return measure.""" - if self.coordinator.data[self._id].measure in [ + if self.coordinator.data.report[self._id].measure in [ PRESSURE_BAR, TEMP_CELSIUS, TEMP_FAHRENHEIT, PERCENTAGE, TIME_HOURS, ]: - return self.coordinator.data[self._id].measure + return self.coordinator.data.report[self._id].measure return None diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json index b06e9188b5b..39ed972524d 100644 --- a/homeassistant/components/atag/strings.json +++ b/homeassistant/components/atag/strings.json @@ -5,7 +5,6 @@ "title": "Connect to the device", "data": { "host": "[%key:common::config_flow::data::host%]", - "email": "[%key:common::config_flow::data::email%]", "port": "[%key:common::config_flow::data::port%]" } } diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 1d28556ba1a..98e947ae643 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { "data": { + "email": "E-mail", "host": "Hoszt", "port": "Port" } diff --git a/homeassistant/components/atag/translations/id.json b/homeassistant/components/atag/translations/id.json new file mode 100644 index 00000000000..24732f8c235 --- /dev/null +++ b/homeassistant/components/atag/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unauthorized": "Pemasangan ditolak, periksa perangkat untuk permintaan autentikasi" + }, + "step": { + "user": { + "data": { + "email": "Email", + "host": "Host", + "port": "Port" + }, + "title": "Hubungkan ke perangkat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index f9c2a4625bb..dac56edf89d 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -35,12 +35,12 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): @property def current_temperature(self): """Return the current temperature.""" - return self.coordinator.atag.dhw.temperature + return self.coordinator.data.dhw.temperature @property def current_operation(self): """Return current operation.""" - operation = self.coordinator.atag.dhw.current_operation + operation = self.coordinator.data.dhw.current_operation return operation if operation in self.operation_list else STATE_OFF @property @@ -50,20 +50,20 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - if await self.coordinator.atag.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): + if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): self.async_write_ha_state() @property def target_temperature(self): """Return the setpoint if water demand, otherwise return base temp (comfort level).""" - return self.coordinator.atag.dhw.target_temperature + return self.coordinator.data.dhw.target_temperature @property def max_temp(self): """Return the maximum temperature.""" - return self.coordinator.atag.dhw.max_temp + return self.coordinator.data.dhw.max_temp @property def min_temp(self): """Return the minimum temperature.""" - return self.coordinator.atag.dhw.min_temp + return self.coordinator.data.dhw.min_temp diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 1a8585653fe..d10024f64c2 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -5,7 +5,7 @@ import logging from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -15,7 +15,6 @@ from homeassistant.const import ( POWER_WATT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -215,7 +214,7 @@ class AtomeData: _LOGGER.error("Missing last value in values: %s: %s", values, error) -class AtomeSensor(Entity): +class AtomeSensor(SensorEntity): """Representation of a sensor entity for Atome.""" def __init__(self, data, name, sensor_type): @@ -243,7 +242,7 @@ class AtomeSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 6f16f7d5b31..46acd1132d9 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,172 +1,37 @@ """Support for August devices.""" import asyncio -import itertools +from itertools import chain import logging from aiohttp import ClientError, ClientResponseError -from august.authenticator import ValidationResult -from august.exceptions import AugustApiAIOHTTPError -import voluptuous as vol +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_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_TIMEOUT, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -import homeassistant.helpers.config_validation as cv from .activity import ActivityStream -from .const import ( - AUGUST_COMPONENTS, - CONF_ACCESS_TOKEN_CACHE_FILE, - CONF_INSTALL_ID, - CONF_LOGIN_METHOD, - DATA_AUGUST, - DEFAULT_AUGUST_CONFIG_FILE, - DEFAULT_NAME, - DEFAULT_TIMEOUT, - DOMAIN, - LOGIN_METHODS, - MIN_TIME_BETWEEN_DETAIL_UPDATES, - VERIFICATION_CODE_KEY, -) +from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .gateway import AugustGateway from .subscriber import AugustSubscriberMixin _LOGGER = logging.getLogger(__name__) -TWO_FA_REVALIDATE = "verify_configurator" - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_INSTALL_ID): cv.string, - vol.Optional( - CONF_TIMEOUT, default=DEFAULT_TIMEOUT - ): cv.positive_int, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, +API_CACHED_ATTRS = ( + "door_state", + "door_state_datetime", + "lock_status", + "lock_status_datetime", ) -async def async_request_validation(hass, config_entry, august_gateway): - """Request a new verification code from the user.""" - - # - # In the future this should start a new config flow - # instead of using the legacy configurator - # - _LOGGER.error("Access token is no longer valid") - configurator = hass.components.configurator - entry_id = config_entry.entry_id - - async def async_august_configuration_validation_callback(data): - code = data.get(VERIFICATION_CODE_KEY) - result = await august_gateway.authenticator.async_validate_verification_code( - code - ) - - if result == ValidationResult.INVALID_VERIFICATION_CODE: - configurator.async_notify_errors( - hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE], - "Invalid verification code, please make sure you are using the latest code and try again.", - ) - elif result == ValidationResult.VALIDATED: - return await async_setup_august(hass, config_entry, august_gateway) - - return False - - if TWO_FA_REVALIDATE not in hass.data[DOMAIN][entry_id]: - await august_gateway.authenticator.async_send_verification_code() - - entry_data = config_entry.data - login_method = entry_data.get(CONF_LOGIN_METHOD) - username = entry_data.get(CONF_USERNAME) - - hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE] = configurator.async_request_config( - f"{DEFAULT_NAME} ({username})", - async_august_configuration_validation_callback, - description=( - "August must be re-verified. " - f"Please check your {login_method} ({username}) " - "and enter the verification code below" - ), - submit_caption="Verify", - fields=[ - {"id": VERIFICATION_CODE_KEY, "name": "Verification code", "type": "string"} - ], - ) - return - - -async def async_setup_august(hass, config_entry, august_gateway): - """Set up the August component.""" - - entry_id = config_entry.entry_id - hass.data[DOMAIN].setdefault(entry_id, {}) - - try: - await august_gateway.async_authenticate() - except RequireValidation: - await async_request_validation(hass, config_entry, august_gateway) - raise - - # We still use the configurator to get a new 2fa code - # when needed since config_flow doesn't have a way - # to re-request if it expires - if TWO_FA_REVALIDATE in hass.data[DOMAIN][entry_id]: - hass.components.configurator.async_request_done( - hass.data[DOMAIN][entry_id].pop(TWO_FA_REVALIDATE) - ) - - hass.data[DOMAIN][entry_id][DATA_AUGUST] = AugustData(hass, august_gateway) - - await hass.data[DOMAIN][entry_id][DATA_AUGUST].async_setup() - - for component in AUGUST_COMPONENTS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) - - return True - - async def async_setup(hass: HomeAssistant, config: dict): """Set up the August component from YAML.""" - - conf = config.get(DOMAIN) hass.data.setdefault(DOMAIN, {}) - - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_LOGIN_METHOD: conf.get(CONF_LOGIN_METHOD), - CONF_USERNAME: conf.get(CONF_USERNAME), - CONF_PASSWORD: conf.get(CONF_PASSWORD), - CONF_INSTALL_ID: conf.get(CONF_INSTALL_ID), - CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE, - }, - ) - ) return True @@ -184,11 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False raise ConfigEntryNotReady from err - except InvalidAuth: + except (RequireValidation, InvalidAuth): _async_start_reauth(hass, entry) return False - except RequireValidation: - return False except (CannotConnect, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err @@ -197,7 +60,7 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=entry.data, ) ) @@ -206,11 +69,14 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + + hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() + unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in AUGUST_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -221,6 +87,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok +async def async_setup_august(hass, config_entry, august_gateway): + """Set up the August component.""" + + if CONF_PASSWORD in config_entry.data: + # We no longer need to store passwords since we do not + # support YAML anymore + config_data = config_entry.data.copy() + del config_data[CONF_PASSWORD] + hass.config_entries.async_update_entry(config_entry, data=config_data) + + await august_gateway.async_authenticate() + + 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) + ) + + return True + + class AugustData(AugustSubscriberMixin): """August data object.""" @@ -235,25 +126,27 @@ class AugustData(AugustSubscriberMixin): self._doorbells_by_id = {} self._locks_by_id = {} self._house_ids = set() + self._pubnub_unsub = None async def async_setup(self): """Async setup of august device data and activities.""" - locks = ( - await self._api.async_get_operable_locks(self._august_gateway.access_token) - or [] - ) - doorbells = ( - await self._api.async_get_doorbells(self._august_gateway.access_token) or [] + token = self._august_gateway.access_token + user_data, locks, doorbells = await asyncio.gather( + self._api.async_get_user(token), + self._api.async_get_operable_locks(token), + self._api.async_get_doorbells(token), ) + if not doorbells: + doorbells = [] + if not locks: + locks = [] self._doorbells_by_id = {device.device_id: device for device in doorbells} self._locks_by_id = {device.device_id: device for device in locks} - self._house_ids = { - device.house_id for device in itertools.chain(locks, doorbells) - } + self._house_ids = {device.house_id for device in chain(locks, doorbells)} await self._async_refresh_device_detail_by_ids( - [device.device_id for device in itertools.chain(locks, doorbells)] + [device.device_id for device in chain(locks, doorbells)] ) # We remove all devices that we are missing @@ -263,10 +156,32 @@ class AugustData(AugustSubscriberMixin): self._remove_inoperative_locks() self._remove_inoperative_doorbells() + pubnub = AugustPubNub() + for device in self._device_detail_by_id.values(): + pubnub.register_device(device) + self.activity_stream = ActivityStream( - self._hass, self._api, self._august_gateway, self._house_ids + self._hass, self._api, self._august_gateway, self._house_ids, pubnub ) await self.activity_stream.async_setup() + pubnub.subscribe(self.async_pubnub_message) + self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub) + + @callback + def async_pubnub_message(self, device_id, date_time, message): + """Process a pubnub message.""" + device = self.get_device_detail(device_id) + activities = activities_from_pubnub_message(device, date_time, message) + if activities: + self.activity_stream.async_process_newer_device_activities(activities) + self.async_signal_device_id_update(device.device_id) + self.activity_stream.async_schedule_house_id_refresh(device.house_id) + + @callback + def async_stop(self): + """Stop the subscriptions.""" + self._pubnub_unsub() + self.activity_stream.async_stop() @property def doorbells(self): @@ -286,27 +201,38 @@ class AugustData(AugustSubscriberMixin): await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) async def _async_refresh_device_detail_by_ids(self, device_ids_list): - for device_id in device_ids_list: - if device_id in self._locks_by_id: - await self._async_update_device_detail( - self._locks_by_id[device_id], self._api.async_get_lock_detail - ) - # keypads are always attached to locks - if ( - device_id in self._device_detail_by_id - and self._device_detail_by_id[device_id].keypad is not None - ): - keypad = self._device_detail_by_id[device_id].keypad - self._device_detail_by_id[keypad.device_id] = keypad - elif device_id in self._doorbells_by_id: - await self._async_update_device_detail( - self._doorbells_by_id[device_id], - self._api.async_get_doorbell_detail, - ) - _LOGGER.debug( - "async_signal_device_id_update (from detail updates): %s", device_id + await asyncio.gather( + *[ + self._async_refresh_device_detail_by_id(device_id) + for device_id in device_ids_list + ] + ) + + async def _async_refresh_device_detail_by_id(self, device_id): + if device_id in self._locks_by_id: + if self.activity_stream and self.activity_stream.pubnub.connected: + saved_attrs = _save_live_attrs(self._device_detail_by_id[device_id]) + await self._async_update_device_detail( + self._locks_by_id[device_id], self._api.async_get_lock_detail ) - self.async_signal_device_id_update(device_id) + if self.activity_stream and self.activity_stream.pubnub.connected: + _restore_live_attrs(self._device_detail_by_id[device_id], saved_attrs) + # keypads are always attached to locks + if ( + device_id in self._device_detail_by_id + and self._device_detail_by_id[device_id].keypad is not None + ): + keypad = self._device_detail_by_id[device_id].keypad + self._device_detail_by_id[keypad.device_id] = keypad + elif device_id in self._doorbells_by_id: + await self._async_update_device_detail( + self._doorbells_by_id[device_id], + self._api.async_get_doorbell_detail, + ) + _LOGGER.debug( + "async_signal_device_id_update (from detail updates): %s", device_id + ) + self.async_signal_device_id_update(device_id) async def _async_update_device_detail(self, device, api_call): _LOGGER.debug( @@ -334,9 +260,9 @@ class AugustData(AugustSubscriberMixin): def _get_device_name(self, device_id): """Return doorbell or lock name as August has it stored.""" - if self._locks_by_id.get(device_id): + if device_id in self._locks_by_id: return self._locks_by_id[device_id].device_name - if self._doorbells_by_id.get(device_id): + if device_id in self._doorbells_by_id: return self._doorbells_by_id[device_id].device_name async def async_lock(self, device_id): @@ -373,8 +299,7 @@ class AugustData(AugustSubscriberMixin): return ret def _remove_inoperative_doorbells(self): - doorbells = list(self.doorbells) - for doorbell in doorbells: + for doorbell in list(self.doorbells): device_id = doorbell.device_id doorbell_is_operative = False doorbell_detail = self._device_detail_by_id.get(device_id) @@ -394,9 +319,7 @@ class AugustData(AugustSubscriberMixin): # Remove non-operative locks as there must # be a bridge (August Connect) for them to # be usable - locks = list(self.locks) - - for lock in locks: + for lock in list(self.locks): device_id = lock.device_id lock_is_operative = False lock_detail = self._device_detail_by_id.get(device_id) @@ -410,14 +333,27 @@ class AugustData(AugustSubscriberMixin): "The lock %s could not be setup because it does not have a bridge (Connect)", lock.device_name, ) - elif not lock_detail.bridge.operative: - _LOGGER.info( - "The lock %s could not be setup because the bridge (Connect) is not operative", - lock.device_name, - ) + # Bridge may come back online later so we still add the device since we will + # have a pubnub subscription to tell use when it recovers else: lock_is_operative = True if not lock_is_operative: del self._locks_by_id[device_id] del self._device_detail_by_id[device_id] + + +def _save_live_attrs(lock_detail): + """Store the attributes that the lock detail api may have an invalid cache for. + + Since we are connected to pubnub we may have more current data + then the api so we want to restore the most current data after + updating battery state etc. + """ + return {attr: getattr(lock_detail, attr) for attr in API_CACHED_ATTRS} + + +def _restore_live_attrs(lock_detail, attrs): + """Restore the non-cache attributes after a cached update.""" + for attr, value in attrs.items(): + setattr(lock_detail, attr, value) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index d972fbf5281..18f390b4f8f 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -1,8 +1,12 @@ """Consume the august activity stream.""" +import asyncio import logging from aiohttp import ClientError +from homeassistant.core import callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow from .const import ACTIVITY_UPDATE_INTERVAL @@ -17,27 +21,58 @@ ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 class ActivityStream(AugustSubscriberMixin): """August activity stream handler.""" - def __init__(self, hass, api, august_gateway, house_ids): + def __init__(self, hass, api, august_gateway, house_ids, pubnub): """Init August activity stream object.""" super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) self._hass = hass + self._schedule_updates = {} self._august_gateway = august_gateway self._api = api self._house_ids = house_ids - self._latest_activities_by_id_type = {} + self._latest_activities = {} self._last_update_time = None self._abort_async_track_time_interval = None + self.pubnub = pubnub + self._update_debounce = {} async def async_setup(self): """Token refresh check and catch up the activity stream.""" - await self._async_refresh(utcnow) + for house_id in self._house_ids: + self._update_debounce[house_id] = self._async_create_debouncer(house_id) + + await self._async_refresh(utcnow()) + + @callback + def _async_create_debouncer(self, house_id): + """Create a debouncer for the house id.""" + + async def _async_update_house_id(): + await self._async_update_house_id(house_id) + + return Debouncer( + self._hass, + _LOGGER, + cooldown=ACTIVITY_UPDATE_INTERVAL.seconds, + immediate=True, + function=_async_update_house_id, + ) + + @callback + def async_stop(self): + """Cleanup any debounces.""" + for debouncer in self._update_debounce.values(): + debouncer.async_cancel() + for house_id in self._schedule_updates: + if self._schedule_updates[house_id] is not None: + self._schedule_updates[house_id]() + self._schedule_updates[house_id] = None def get_latest_device_activity(self, device_id, activity_types): """Return latest activity that is one of the acitivty_types.""" - if device_id not in self._latest_activities_by_id_type: + if device_id not in self._latest_activities: return None - latest_device_activities = self._latest_activities_by_id_type[device_id] + latest_device_activities = self._latest_activities[device_id] latest_activity = None for activity_type in activity_types: @@ -54,62 +89,86 @@ class ActivityStream(AugustSubscriberMixin): async def _async_refresh(self, time): """Update the activity stream from August.""" - # This is the only place we refresh the api token await self._august_gateway.async_refresh_access_token_if_needed() + if self.pubnub.connected: + _LOGGER.debug("Skipping update because pubnub is connected") + return await self._async_update_device_activities(time) async def _async_update_device_activities(self, time): _LOGGER.debug("Start retrieving device activities") - - limit = ( - ACTIVITY_STREAM_FETCH_LIMIT - if self._last_update_time - else ACTIVITY_CATCH_UP_FETCH_LIMIT + await asyncio.gather( + *[ + self._update_debounce[house_id].async_call() + for house_id in self._house_ids + ] ) - - for house_id in self._house_ids: - _LOGGER.debug("Updating device activity for house id %s", house_id) - try: - activities = await self._api.async_get_house_activities( - self._august_gateway.access_token, house_id, limit=limit - ) - except ClientError as ex: - _LOGGER.error( - "Request error trying to retrieve activity for house id %s: %s", - house_id, - ex, - ) - # Make sure we process the next house if one of them fails - continue - - _LOGGER.debug( - "Completed retrieving device activities for house id %s", house_id - ) - - updated_device_ids = self._process_newer_device_activities(activities) - - if updated_device_ids: - for device_id in updated_device_ids: - _LOGGER.debug( - "async_signal_device_id_update (from activity stream): %s", - device_id, - ) - self.async_signal_device_id_update(device_id) - self._last_update_time = time - def _process_newer_device_activities(self, activities): + @callback + def async_schedule_house_id_refresh(self, house_id): + """Update for a house activities now and once in the future.""" + if self._schedule_updates.get(house_id): + self._schedule_updates[house_id]() + self._schedule_updates[house_id] = None + + async def _update_house_activities(_): + await self._update_debounce[house_id].async_call() + + self._hass.async_create_task(self._update_debounce[house_id].async_call()) + # Schedule an update past the debounce to ensure + # 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 + ) + + async def _async_update_house_id(self, house_id): + """Update device activities for a house.""" + if self._last_update_time: + limit = ACTIVITY_STREAM_FETCH_LIMIT + else: + limit = ACTIVITY_CATCH_UP_FETCH_LIMIT + + _LOGGER.debug("Updating device activity for house id %s", house_id) + try: + activities = await self._api.async_get_house_activities( + self._august_gateway.access_token, house_id, limit=limit + ) + except ClientError as ex: + _LOGGER.error( + "Request error trying to retrieve activity for house id %s: %s", + house_id, + ex, + ) + # Make sure we process the next house if one of them fails + return + + _LOGGER.debug( + "Completed retrieving device activities for house id %s", house_id + ) + + updated_device_ids = self.async_process_newer_device_activities(activities) + + if not updated_device_ids: + return + + for device_id in updated_device_ids: + _LOGGER.debug( + "async_signal_device_id_update (from activity stream): %s", + device_id, + ) + self.async_signal_device_id_update(device_id) + + def async_process_newer_device_activities(self, activities): + """Process activities if they are newer than the last one.""" updated_device_ids = set() for activity in activities: device_id = activity.device_id activity_type = activity.activity_type - - self._latest_activities_by_id_type.setdefault(device_id, {}) - - lastest_activity = self._latest_activities_by_id_type[device_id].get( - activity_type - ) + device_activities = self._latest_activities.setdefault(device_id, {}) + lastest_activity = device_activities.get(activity_type) # Ignore activities that are older than the latest one if ( @@ -118,7 +177,7 @@ class ActivityStream(AugustSubscriberMixin): ): continue - self._latest_activities_by_id_type[device_id][activity_type] = activity + device_activities[activity_type] = activity updated_device_ids.add(device_id) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 226cbf655f9..6dccec57a09 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta import logging -from august.activity import ActivityType -from august.lock import LockDoorStatus -from august.util import update_lock_detail_from_activity +from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, ActivityType +from yalexs.lock import LockDoorStatus +from yalexs.util import update_lock_detail_from_activity from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -14,15 +14,15 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.core import callback -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow +from homeassistant.helpers.event import async_call_later -from .const import DATA_AUGUST, DOMAIN +from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) -TIME_TO_DECLARE_DETECTION = timedelta(seconds=60) +TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds) +TIME_TO_RECHECK_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds * 3) def _retrieve_online_state(data, detail): @@ -35,30 +35,43 @@ def _retrieve_online_state(data, detail): def _retrieve_motion_state(data, detail): - - return _activity_time_based_state( - data, - detail.device_id, - [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING], + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_MOTION} ) + if latest is None: + return False + + return _activity_time_based_state(latest) + def _retrieve_ding_state(data, detail): - - return _activity_time_based_state( - data, detail.device_id, [ActivityType.DOORBELL_DING] + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_DING} ) + if latest is None: + return False -def _activity_time_based_state(data, device_id, activity_types): + if ( + data.activity_stream.pubnub.connected + and latest.action == ACTION_DOORBELL_CALL_MISSED + ): + return False + + return _activity_time_based_state(latest) + + +def _activity_time_based_state(latest): """Get the latest state of the sensor.""" - latest = data.activity_stream.get_latest_device_activity(device_id, activity_types) + start = latest.activity_start_time + end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION + return start <= _native_datetime() <= end - if latest is not None: - start = latest.activity_start_time - end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION - return start <= datetime.now() <= end - return None + +def _native_datetime(): + """Return time in the format august uses without timezone.""" + return datetime.now() SENSOR_NAME = 0 @@ -143,12 +156,19 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): def _update_from_data(self): """Get the latest state of the sensor and update activity.""" door_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.DOOR_OPERATION] + self._device_id, {ActivityType.DOOR_OPERATION} ) if door_activity is not None: update_lock_detail_from_activity(self._detail, door_activity) + bridge_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.BRIDGE_OPERATION} + ) + + if bridge_activity is not None: + update_lock_detail_from_activity(self._detail, bridge_activity) + @property def unique_id(self) -> str: """Get the unique of the door open binary sensor.""" @@ -179,25 +199,30 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state + @property + def _sensor_config(self): + """Return the config for the sensor.""" + return SENSOR_TYPES_DOORBELL[self._sensor_type] + @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS] + return self._sensor_config[SENSOR_DEVICE_CLASS] @property def name(self): """Return the name of the binary sensor.""" - return f"{self._device.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}" + return f"{self._device.device_name} {self._sensor_config[SENSOR_NAME]}" @property def _state_provider(self): """Return the state provider for the binary sensor.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_PROVIDER] + return self._sensor_config[SENSOR_STATE_PROVIDER] @property def _is_time_based(self): """Return true of false if the sensor is time based.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_IS_TIME_BASED] + return self._sensor_config[SENSOR_STATE_IS_TIME_BASED] @callback def _update_from_data(self): @@ -228,17 +253,20 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Timer callback for sensor update.""" self._check_for_off_update_listener = None self._update_from_data() + if not self._state: + self.async_write_ha_state() - self._check_for_off_update_listener = async_track_point_in_utc_time( - self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION + self._check_for_off_update_listener = async_call_later( + self.hass, TIME_TO_RECHECK_DETECTION.seconds, _scheduled_update ) def _cancel_any_pending_updates(self): """Cancel any updates to recheck a sensor to see if it is ready to turn off.""" - if self._check_for_off_update_listener: - _LOGGER.debug("%s: canceled pending update", self.entity_id) - self._check_for_off_update_listener() - self._check_for_off_update_listener = None + if not self._check_for_off_update_listener: + return + _LOGGER.debug("%s: canceled pending update", self.entity_id) + self._check_for_off_update_listener() + self._check_for_off_update_listener = None async def async_added_to_hass(self): """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" @@ -248,7 +276,4 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): @property def unique_id(self) -> str: """Get the unique id of the doorbell sensor.""" - return ( - f"{self._device_id}_" - f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}" - ) + return f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}" diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 4037489fa22..e002e0b2517 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -1,7 +1,7 @@ """Support for August doorbell camera.""" -from august.activity import ActivityType -from august.util import update_doorbell_image_from_activity +from yalexs.activity import ActivityType +from yalexs.util import update_doorbell_image_from_activity from homeassistant.components.camera import Camera from homeassistant.core import callback @@ -63,7 +63,7 @@ class AugustCamera(AugustEntityMixin, Camera): def _update_from_data(self): """Get the latest state of the sensor.""" doorbell_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.DOORBELL_MOTION] + self._device_id, {ActivityType.DOORBELL_MOTION} ) if doorbell_activity is not None: diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index f595479c0cf..0138b438a1e 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -1,19 +1,13 @@ """Config flow for August integration.""" import logging -from august.authenticator import ValidationResult import voluptuous as vol +from yalexs.authenticator import ValidationResult from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import ( - CONF_LOGIN_METHOD, - DEFAULT_TIMEOUT, - LOGIN_METHODS, - VERIFICATION_CODE_KEY, -) -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_LOGIN_METHOD, DOMAIN, LOGIN_METHODS, VERIFICATION_CODE_KEY from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .gateway import AugustGateway @@ -68,61 +62,48 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Store an AugustGateway().""" self._august_gateway = None - self.user_auth_details = {} + self._user_auth_details = {} self._needs_reset = False + self._mode = None super().__init__() async def async_step_user(self, user_input=None): """Handle the initial step.""" - if self._august_gateway is None: - self._august_gateway = AugustGateway(self.hass) + self._august_gateway = AugustGateway(self.hass) + return await self.async_step_user_validate() + + async def async_step_user_validate(self, user_input=None): + """Handle authentication.""" errors = {} if user_input is not None: - combined_inputs = {**self.user_auth_details, **user_input} - await self._august_gateway.async_setup(combined_inputs) - if self._needs_reset: - self._needs_reset = False - await self._august_gateway.async_reset_authentication() - - try: - info = await async_validate_input( - combined_inputs, - self._august_gateway, - ) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except RequireValidation: - self.user_auth_details.update(user_input) - - return await self.async_step_validation() - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if not errors: - self.user_auth_details.update(user_input) - - existing_entry = await self.async_set_unique_id( - combined_inputs[CONF_USERNAME] - ) - if existing_entry: - self.hass.config_entries.async_update_entry( - existing_entry, data=info["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=info["title"], data=info["data"]) + result = await self._async_auth_or_validate(user_input, errors) + if result is not None: + return result return self.async_show_form( - step_id="user", data_schema=self._async_build_schema(), errors=errors + step_id="user_validate", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOGIN_METHOD, + default=self._user_auth_details.get(CONF_LOGIN_METHOD, "phone"), + ): vol.In(LOGIN_METHODS), + vol.Required( + CONF_USERNAME, + default=self._user_auth_details.get(CONF_USERNAME), + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, ) async def async_step_validation(self, user_input=None): """Handle validation (2fa) step.""" if user_input: - return await self.async_step_user({**self.user_auth_details, **user_input}) + if self._mode == "reauth": + return await self.async_step_reauth_validate(user_input) + return await self.async_step_user_validate(user_input) return self.async_show_form( step_id="validation", @@ -130,34 +111,70 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): {vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)} ), description_placeholders={ - CONF_USERNAME: self.user_auth_details.get(CONF_USERNAME), - CONF_LOGIN_METHOD: self.user_auth_details.get(CONF_LOGIN_METHOD), + CONF_USERNAME: self._user_auth_details[CONF_USERNAME], + CONF_LOGIN_METHOD: self._user_auth_details[CONF_LOGIN_METHOD], }, ) - async def async_step_import(self, user_input): - """Handle import.""" - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input) - async def async_step_reauth(self, data): """Handle configuration by re-auth.""" - self.user_auth_details = dict(data) + self._user_auth_details = dict(data) + self._mode = "reauth" self._needs_reset = True - return await self.async_step_user() + self._august_gateway = AugustGateway(self.hass) + return await self.async_step_reauth_validate() - def _async_build_schema(self): - """Generate the config flow schema.""" - base_schema = { - vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int), - } - for key in self.user_auth_details: - if key == CONF_PASSWORD or key not in base_schema: - continue - del base_schema[key] - return vol.Schema(base_schema) + async def async_step_reauth_validate(self, user_input=None): + """Handle reauth and validation.""" + errors = {} + if user_input is not None: + result = await self._async_auth_or_validate(user_input, errors) + if result is not None: + return result + + return self.async_show_form( + step_id="reauth_validate", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._user_auth_details[CONF_USERNAME], + }, + ) + + async def _async_auth_or_validate(self, user_input, errors): + self._user_auth_details.update(user_input) + await self._august_gateway.async_setup(self._user_auth_details) + if self._needs_reset: + self._needs_reset = False + await self._august_gateway.async_reset_authentication() + try: + info = await async_validate_input( + self._user_auth_details, + self._august_gateway, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except RequireValidation: + return await self.async_step_validation() + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if errors: + return None + + existing_entry = await self.async_set_unique_id( + self._user_auth_details[CONF_USERNAME] + ) + if not existing_entry: + return self.async_create_entry(title=info["title"], data=info["data"]) + + self.hass.config_entries.async_update_entry(existing_entry, data=info["data"]) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index a32e187647a..57e0d5a7fb7 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -43,4 +43,4 @@ ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10) LOGIN_METHODS = ["phone", "email"] -AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock", "sensor"] +PLATFORMS = ["camera", "binary_sensor", "lock", "sensor"] diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index b6c677a63b6..b2a93948449 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -5,6 +5,8 @@ from homeassistant.helpers.entity import Entity from . import DOMAIN from .const import MANUFACTURER +DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"] + class AugustEntityMixin(Entity): """Base implementation for August device.""" @@ -31,12 +33,14 @@ class AugustEntityMixin(Entity): @property def device_info(self): """Return the device_info of the device.""" + name = self._device.device_name return { "identifiers": {(DOMAIN, self._device_id)}, - "name": self._device.device_name, + "name": name, "manufacturer": MANUFACTURER, "sw_version": self._detail.firmware_version, "model": self._detail.model, + "suggested_area": _remove_device_types(name, DEVICE_TYPES), } @callback @@ -56,3 +60,19 @@ class AugustEntityMixin(Entity): self._device_id, self._update_from_data_and_write_state ) ) + + +def _remove_device_types(name, device_types): + """Strip device types from a string. + + August stores the name as Master Bed Lock + or Master Bed Door. We can come up with a + reasonable suggestion by removing the supported + device types from the string. + """ + lower_name = name.lower() + for device_type in device_types: + device_type_with_space = f" {device_type}" + if lower_name.endswith(device_type_with_space): + lower_name = lower_name[: -len(device_type_with_space)] + return name[: len(lower_name)] diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index b72bb52e710..5499246a187 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -5,8 +5,8 @@ import logging import os from aiohttp import ClientError, ClientResponseError -from august.api_async import ApiAsync -from august.authenticator_async import AuthenticationState, AuthenticatorAsync +from yalexs.api_async import ApiAsync +from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync from homeassistant.const import ( CONF_PASSWORD, @@ -21,6 +21,7 @@ from .const import ( CONF_INSTALL_ID, CONF_LOGIN_METHOD, DEFAULT_AUGUST_CONFIG_FILE, + DEFAULT_TIMEOUT, VERIFICATION_CODE_KEY, ) from .exceptions import CannotConnect, InvalidAuth, RequireValidation @@ -52,9 +53,7 @@ class AugustGateway: return { CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD], CONF_USERNAME: self._config[CONF_USERNAME], - CONF_PASSWORD: self._config[CONF_PASSWORD], CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID), - CONF_TIMEOUT: self._config.get(CONF_TIMEOUT), CONF_ACCESS_TOKEN_CACHE_FILE: self._access_token_cache_file, } @@ -70,14 +69,15 @@ class AugustGateway: self._config = conf self.api = ApiAsync( - self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT) + self._aiohttp_session, + timeout=self._config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), ) self.authenticator = AuthenticatorAsync( self.api, self._config[CONF_LOGIN_METHOD], self._config[CONF_USERNAME], - self._config[CONF_PASSWORD], + self._config.get(CONF_PASSWORD, ""), install_id=self._config.get(CONF_INSTALL_ID), access_token_cache_file=self._hass.config.path( self._access_token_cache_file @@ -128,14 +128,15 @@ class AugustGateway: async def async_refresh_access_token_if_needed(self): """Refresh the august access token if needed.""" - if self.authenticator.should_refresh(): - async with self._token_refresh_lock: - refreshed_authentication = ( - await self.authenticator.async_refresh_access_token(force=False) - ) - _LOGGER.info( - "Refreshed august access token. The old token expired at %s, and the new token expires at %s", - self.authentication.access_token_expires, - refreshed_authentication.access_token_expires, - ) - self.authentication = refreshed_authentication + if not self.authenticator.should_refresh(): + return + async with self._token_refresh_lock: + refreshed_authentication = ( + await self.authenticator.async_refresh_access_token(force=False) + ) + _LOGGER.info( + "Refreshed august access token. The old token expired at %s, and the new token expires at %s", + self.authentication.access_token_expires, + refreshed_authentication.access_token_expires, + ) + self.authentication = refreshed_authentication diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e16c603d919..59c97190d7f 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,9 +1,9 @@ """Support for August lock.""" import logging -from august.activity import ActivityType -from august.lock import LockStatus -from august.util import update_lock_detail_from_activity +from yalexs.activity import ActivityType +from yalexs.lock import LockStatus +from yalexs.util import update_lock_detail_from_activity from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL @@ -73,13 +73,21 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): def _update_from_data(self): """Get the latest state of the sensor and update activity.""" lock_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.LOCK_OPERATION] + self._device_id, + {ActivityType.LOCK_OPERATION, ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR}, ) if lock_activity is not None: self._changed_by = lock_activity.operated_by update_lock_detail_from_activity(self._detail, lock_activity) + bridge_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.BRIDGE_OPERATION} + ) + + if bridge_activity is not None: + update_lock_detail_from_activity(self._detail, bridge_activity) + self._update_lock_status_from_detail() @property @@ -105,7 +113,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): return self._changed_by @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attributes = {ATTR_BATTERY_LEVEL: self._detail.battery_level} diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index dcdfb0a0497..fb4ff1a3484 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,12 +2,12 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.25.2"], - "dependencies": ["configurator"], + "requirements": ["yalexs==1.1.10"], "codeowners": ["@bdraco"], "dhcp": [ {"hostname":"connect","macaddress":"D86162*"}, - {"hostname":"connect","macaddress":"B8B7F1*"} + {"hostname":"connect","macaddress":"B8B7F1*"}, + {"hostname":"august*","macaddress":"E076D0*"} ], "config_flow": true } diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 6004a07f605..44597a6485e 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -1,12 +1,11 @@ """Support for August sensors.""" import logging -from august.activity import ActivityType +from yalexs.activity import ActivityType -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity @@ -118,7 +117,7 @@ async def _async_migrate_old_unique_ids(hass, devices): registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id) -class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): +class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): """Representation of an August lock operation sensor.""" def __init__(self, data, device): @@ -154,7 +153,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): def _update_from_data(self): """Get the latest state of the sensor and update activity.""" lock_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.LOCK_OPERATION] + self._device_id, {ActivityType.LOCK_OPERATION} ) self._available = True @@ -166,7 +165,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): self._entity_picture = lock_activity.operator_thumbnail_url @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attributes = {} @@ -217,7 +216,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): return f"{self._device_id}_lock_operator" -class AugustBatterySensor(AugustEntityMixin, Entity): +class AugustBatterySensor(AugustEntityMixin, SensorEntity): """Representation of an August sensor.""" def __init__(self, data, sensor_type, device, old_device): diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 998d870e629..7939fb1d25f 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -17,15 +17,21 @@ }, "description": "Please check your {login_method} ({username}) and enter the verification code below" }, - "user": { + "user_validate": { "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", "data": { - "timeout": "Timeout (seconds)", "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]", "login_method": "Login Method" }, "title": "Setup an August account" + }, + "reauth_validate": { + "description": "Enter the password for {username}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "Reauthenticate an August account" } } } diff --git a/homeassistant/components/august/translations/ca.json b/homeassistant/components/august/translations/ca.json index 9d3e3535e46..8faa12e2757 100644 --- a/homeassistant/components/august/translations/ca.json +++ b/homeassistant/components/august/translations/ca.json @@ -10,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_validate": { + "data": { + "password": "Contrasenya" + }, + "description": "Introdueix la contrasenya per a {username}.", + "title": "Torna a autenticar compte d'August" + }, "user": { "data": { "login_method": "M\u00e8tode d'inici de sessi\u00f3", @@ -20,12 +27,21 @@ "description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en el format \"+NNNNNNNNN\".", "title": "Configuraci\u00f3 de compte August" }, + "user_validate": { + "data": { + "login_method": "M\u00e8tode d'inici de sessi\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'email', el nom d'usuari \u00e9s l'adre\u00e7a de correu electr\u00f2nic. Si el m\u00e8tode d'inici de sessi\u00f3 \u00e9s 'phone', el nom d'usuari \u00e9s el n\u00famero de tel\u00e8fon en format \"+NNNNNNNNN\".", + "title": "Configuraci\u00f3 de compte August" + }, "validation": { "data": { "code": "Codi de verificaci\u00f3" }, "description": "Comprova el teu {login_method} ({username}) i introdueix el codi de verificaci\u00f3 a continuaci\u00f3", - "title": "Autenticaci\u00f3 de dos factors" + "title": "Verificaci\u00f3 en dos passos" } } } diff --git a/homeassistant/components/august/translations/cs.json b/homeassistant/components/august/translations/cs.json index 4100014abb6..4176da8f1bf 100644 --- a/homeassistant/components/august/translations/cs.json +++ b/homeassistant/components/august/translations/cs.json @@ -10,6 +10,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_validate": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "login_method": "Zp\u016fsob p\u0159ihl\u00e1\u0161en\u00ed", @@ -20,6 +25,12 @@ "description": "Pokud je metoda p\u0159ihl\u00e1\u0161en\u00ed \"e-mail\", je e-mailovou adresou u\u017eivatelsk\u00e9 jm\u00e9no. Pokud je p\u0159ihla\u0161ovac\u00ed metoda \"telefon\", u\u017eivatelsk\u00e9 jm\u00e9no je telefonn\u00ed \u010d\u00edslo ve form\u00e1tu \"+NNNNNNNNN\".", "title": "Nastavte \u00fa\u010det August" }, + "user_validate": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, "validation": { "data": { "code": "Ov\u011b\u0159ovac\u00ed k\u00f3d" diff --git a/homeassistant/components/august/translations/de.json b/homeassistant/components/august/translations/de.json index 3a5bd70f1af..ef525fb665d 100644 --- a/homeassistant/components/august/translations/de.json +++ b/homeassistant/components/august/translations/de.json @@ -10,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_validate": { + "data": { + "password": "Passwort" + }, + "description": "Gib das Passwort f\u00fcr {username} ein.", + "title": "August-Konto erneut authentifizieren" + }, "user": { "data": { "login_method": "Anmeldemethode", @@ -20,6 +27,15 @@ "description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".", "title": "Richten Sie ein August-Konto ein" }, + "user_validate": { + "data": { + "login_method": "Anmeldemethode", + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".", + "title": "Richte ein August-Konto ein" + }, "validation": { "data": { "code": "Verifizierungs-Code" diff --git a/homeassistant/components/august/translations/el.json b/homeassistant/components/august/translations/el.json new file mode 100644 index 00000000000..8a976d451be --- /dev/null +++ b/homeassistant/components/august/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "reauth_validate": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} .", + "title": "\u0395\u03c0\u03b9\u03ba\u03c5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03ad\u03bd\u03b1\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc August" + }, + "user_validate": { + "data": { + "login_method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03ac\u03bd \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"\u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\", \u03c4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5. \u0395\u03ac\u03bd \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \"\u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03bf\", \u03c4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5 \u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae '+NNNNNNNNNN'.", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc August" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json index c6c19321d8a..0b8d1511244 100644 --- a/homeassistant/components/august/translations/en.json +++ b/homeassistant/components/august/translations/en.json @@ -10,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "Enter the password for {username}.", + "title": "Reauthenticate an August account" + }, "user": { "data": { "login_method": "Login Method", @@ -20,6 +27,15 @@ "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", "title": "Setup an August account" }, + "user_validate": { + "data": { + "login_method": "Login Method", + "password": "Password", + "username": "Username" + }, + "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", + "title": "Setup an August account" + }, "validation": { "data": { "code": "Verification code" diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index a8f7bc6af23..bb343e6da97 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -10,6 +10,10 @@ "unknown": "Error inesperado" }, "step": { + "reauth_validate": { + "description": "Introduzca la contrase\u00f1a de {username}.", + "title": "Reautorizar una cuenta de August" + }, "user": { "data": { "login_method": "M\u00e9todo de inicio de sesi\u00f3n", @@ -20,6 +24,13 @@ "description": "Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'correo electr\u00f3nico', Usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de Inicio de Sesi\u00f3n es 'tel\u00e9fono', Usuario es el n\u00famero de tel\u00e9fono en formato '+NNNNNNNNN'.", "title": "Configurar una cuenta de August" }, + "user_validate": { + "data": { + "login_method": "M\u00e9todo de inicio de sesi\u00f3n" + }, + "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" + }, "validation": { "data": { "code": "C\u00f3digo de verificaci\u00f3n" diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json index 0b455b06d00..69cd9e66ce3 100644 --- a/homeassistant/components/august/translations/et.json +++ b/homeassistant/components/august/translations/et.json @@ -10,6 +10,13 @@ "unknown": "Tundmatu viga" }, "step": { + "reauth_validate": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta kasutaja {username} salas\u00f5na.", + "title": "Autendi Augusti konto uuesti" + }, "user": { "data": { "login_method": "Sisselogimismeetod", @@ -20,6 +27,15 @@ "description": "Kui sisselogimismeetod on \"e-post\" on kasutajanimi e-posti aadress. Kui sisselogimismeetod on \"telefon\" on kasutajanimi telefoninumber vormingus \"+NNNNNNNNN\".", "title": "Seadista Augusti sidumise konto" }, + "user_validate": { + "data": { + "login_method": "Sisselogimismeetod", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Kui sisselogimismeetod on \"e-post\" on kasutajanimi e-posti aadress. Kui sisselogimismeetod on \"telefon\" on kasutajanimi telefoninumber vormingus \"+NNNNNNNNN\".", + "title": "Seadista Augusti sidumise konto" + }, "validation": { "data": { "code": "Kinnituskood" diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json index 82568b681fd..967fb249d97 100644 --- a/homeassistant/components/august/translations/fr.json +++ b/homeassistant/components/august/translations/fr.json @@ -10,6 +10,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_validate": { + "data": { + "password": "Mot de passe" + }, + "description": "Saisissez le mot de passe de {username} .", + "title": "R\u00e9authentifier un compte August" + }, "user": { "data": { "login_method": "M\u00e9thode de connexion", @@ -20,6 +27,15 @@ "description": "Si la m\u00e9thode de connexion est \u00abe-mail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.", "title": "Configurer un compte August" }, + "user_validate": { + "data": { + "login_method": "M\u00e9thode de connexion", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Si la m\u00e9thode de connexion est \u00abemail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.", + "title": "Cr\u00e9er un compte August" + }, "validation": { "data": { "code": "Code de v\u00e9rification" diff --git a/homeassistant/components/august/translations/he.json b/homeassistant/components/august/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/august/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index dee4ed9ee0f..1bced4e1036 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -1,11 +1,42 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { + "reauth_validate": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "Add meg a(z) {username} jelszav\u00e1t.", + "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (m\u00e1sodperc)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } + }, + "user_validate": { + "data": { + "login_method": "Bejelentkez\u00e9si m\u00f3d", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "August fi\u00f3k be\u00e1ll\u00edt\u00e1sa" + }, + "validation": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d" + }, + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" } } } diff --git a/homeassistant/components/august/translations/id.json b/homeassistant/components/august/translations/id.json new file mode 100644 index 00000000000..a66c43ce057 --- /dev/null +++ b/homeassistant/components/august/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "login_method": "Metode Masuk", + "password": "Kata Sandi", + "timeout": "Tenggang waktu (detik)", + "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" + }, + "description": "Periksa {login_method} ({username}) Anda dan masukkan kode verifikasi di bawah ini", + "title": "Autentikasi dua faktor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json index adc9017a275..c20f95b90ad 100644 --- a/homeassistant/components/august/translations/it.json +++ b/homeassistant/components/august/translations/it.json @@ -10,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "Inserisci la password per {username}.", + "title": "Riautentica un account di August" + }, "user": { "data": { "login_method": "Metodo di accesso", @@ -20,6 +27,15 @@ "description": "Se il metodo di accesso \u00e8 \"e-mail\", il nome utente \u00e8 l'indirizzo e-mail. Se il metodo di accesso \u00e8 \"telefono\", il nome utente \u00e8 il numero di telefono nel formato \"+NNNNNNNNN\".", "title": "Configura un account di August" }, + "user_validate": { + "data": { + "login_method": "Metodo di accesso", + "password": "Password", + "username": "Nome utente" + }, + "description": "Se il metodo di accesso \u00e8 'email', il nome utente \u00e8 l'indirizzo email. Se il metodo di accesso \u00e8 'phone', il nome utente \u00e8 il numero di telefono nel formato '+NNNNNNNNNN'.", + "title": "Configura un account di August" + }, "validation": { "data": { "code": "Codice di verifica" diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json index e7aed3d4c2c..f3bc64a706c 100644 --- a/homeassistant/components/august/translations/ko.json +++ b/homeassistant/components/august/translations/ko.json @@ -10,6 +10,13 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_validate": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "{username}\uc758 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "August \uacc4\uc815 \uc7ac\uc778\uc99d\ud558\uae30" + }, "user": { "data": { "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95", @@ -20,6 +27,15 @@ "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30" }, + "user_validate": { + "data": { + "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", + "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30" + }, "validation": { "data": { "code": "\uc778\uc99d \ucf54\ub4dc" diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json index e48d27801cc..05a5a4c5265 100644 --- a/homeassistant/components/august/translations/nl.json +++ b/homeassistant/components/august/translations/nl.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Account al geconfigureerd", + "already_configured": "Account is al geconfigureerd", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { + "reauth_validate": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord in voor {username} .", + "title": "Verifieer een August-account opnieuw" + }, "user": { "data": { "login_method": "Aanmeldmethode", @@ -20,6 +27,15 @@ "description": "Als de aanmeldingsmethode 'e-mail' is, is gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.", "title": "Stel een augustus-account in" }, + "user_validate": { + "data": { + "login_method": "Inlogmethode", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Als de aanmeldingsmethode 'e-mail' is, is de gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is de gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.", + "title": "Stel een August account in" + }, "validation": { "data": { "code": "Verificatiecode" diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index ae314897e74..d90e7f8080a 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -10,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_validate": { + "data": { + "password": "Passord" + }, + "description": "Skriv inn passordet for {username} .", + "title": "Godkjenn en August-konto p\u00e5 nytt" + }, "user": { "data": { "login_method": "P\u00e5loggingsmetode", @@ -20,6 +27,15 @@ "description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.", "title": "Sett opp en August konto" }, + "user_validate": { + "data": { + "login_method": "P\u00e5loggingsmetode", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Hvis p\u00e5loggingsmetoden er 'e-post', er brukernavnet e-postadressen. Hvis p\u00e5loggingsmetoden er 'telefon', er brukernavn telefonnummeret i formatet '+ NNNNNNNNN'.", + "title": "Sett opp en August konto" + }, "validation": { "data": { "code": "Bekreftelseskode" diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json index e76b663c307..a5539bea93a 100644 --- a/homeassistant/components/august/translations/pl.json +++ b/homeassistant/components/august/translations/pl.json @@ -10,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_validate": { + "data": { + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a has\u0142o dla {username}", + "title": "Ponownie uwierzytelnij konto August" + }, "user": { "data": { "login_method": "Metoda logowania", @@ -20,6 +27,15 @@ "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.", "title": "Konfiguracja konta August" }, + "user_validate": { + "data": { + "login_method": "Metoda logowania", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.", + "title": "Konfiguracja konta August" + }, "validation": { "data": { "code": "Kod weryfikacyjny" diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json index 97dba8fc758..0263ef6ee18 100644 --- a/homeassistant/components/august/translations/ru.json +++ b/homeassistant/components/august/translations/ru.json @@ -10,12 +10,28 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + }, "user": { "data": { "login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.", + "title": "August" + }, + "user_validate": { + "data": { + "login_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0415\u0441\u043b\u0438 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0438\u043d\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 '+NNNNNNNNN'.", "title": "August" diff --git a/homeassistant/components/august/translations/zh-Hant.json b/homeassistant/components/august/translations/zh-Hant.json index 667d8814659..ab157e3da3c 100644 --- a/homeassistant/components/august/translations/zh-Hant.json +++ b/homeassistant/components/august/translations/zh-Hant.json @@ -10,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_validate": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8f38\u5165{username} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49 August \u5e33\u865f" + }, "user": { "data": { "login_method": "\u767b\u5165\u65b9\u5f0f", @@ -20,12 +27,21 @@ "description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002", "title": "\u8a2d\u5b9a August \u5e33\u865f" }, + "user_validate": { + "data": { + "login_method": "\u767b\u5165\u65b9\u5f0f", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u90f5\u4ef6\u300cemail\u300d\u3001\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002\u5047\u5982\u767b\u5165\u65b9\u5f0f\u70ba\u96fb\u8a71\u300cphone\u300d\u3001\u5247\u4f7f\u7528\u8005\u540d\u7a31\u70ba\u5305\u542b\u570b\u78bc\u4e4b\u96fb\u8a71\u865f\u78bc\uff0c\u5982\u300c+NNNNNNNNN\u300d\u3002", + "title": "\u8a2d\u5b9a August \u5e33\u865f" + }, "validation": { "data": { "code": "\u9a57\u8b49\u78bc" }, "description": "\u8acb\u78ba\u8a8d {login_method} ({username}) \u4e26\u65bc\u4e0b\u65b9\u8f38\u5165\u9a57\u8b49\u78bc", - "title": "\u5169\u6b65\u9a5f\u9a57\u8b49" + "title": "\u96d9\u91cd\u8a8d\u8b49" } } } diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index a187288e2e4..8823cf1c8ec 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -10,7 +10,6 @@ from auroranoaa import AuroraForecast from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -37,13 +36,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Aurora component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Aurora from a config entry.""" @@ -69,19 +61,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): threshold=threshold, ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, AURORA_API: api, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -92,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -168,7 +158,7 @@ class AuroraEntity(CoordinatorEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"attribution": ATTRIBUTION} diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py index 24161c059c1..6e2396d3f5c 100644 --- a/homeassistant/components/aurora/config_flow.py +++ b/homeassistant/components/aurora/config_flow.py @@ -10,12 +10,7 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import ( # pylint: disable=unused-import - CONF_THRESHOLD, - DEFAULT_NAME, - DEFAULT_THRESHOLD, - DOMAIN, -) +from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index 731c6d08afd..d7024cc630a 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -1,4 +1,5 @@ """Support for Aurora Forecast sensor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import PERCENTAGE from . import AuroraEntity @@ -18,7 +19,7 @@ async def async_setup_entry(hass, entry, async_add_entries): async_add_entries([entity]) -class AuroraSensor(AuroraEntity): +class AuroraSensor(AuroraEntity, SensorEntity): """Implementation of an aurora sensor.""" @property diff --git a/homeassistant/components/aurora/translations/hu.json b/homeassistant/components/aurora/translations/hu.json index d5363860cbd..292ed552235 100644 --- a/homeassistant/components/aurora/translations/hu.json +++ b/homeassistant/components/aurora/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni" + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { diff --git a/homeassistant/components/aurora/translations/id.json b/homeassistant/components/aurora/translations/id.json new file mode 100644 index 00000000000..66cf534b7ae --- /dev/null +++ b/homeassistant/components/aurora/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Ambang (%)" + } + } + } + }, + "title": "Sensor Aurora NOAA" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/ko.json b/homeassistant/components/aurora/translations/ko.json index ea10c059f03..39f3c4d4281 100644 --- a/homeassistant/components/aurora/translations/ko.json +++ b/homeassistant/components/aurora/translations/ko.json @@ -12,5 +12,15 @@ } } } - } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\uc784\uacc4\uac12 (%)" + } + } + } + }, + "title": "NOAA Aurora \uc13c\uc11c" } \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 69a513dd8fb..f4640e7c014 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -5,7 +5,7 @@ import logging from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, @@ -14,7 +14,6 @@ from homeassistant.const import ( POWER_WATT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class AuroraABBSolarPVMonitorSensor(Entity): +class AuroraABBSolarPVMonitorSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, client, name, typename): diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 4ddf82cc022..7381be5e9de 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -114,8 +114,9 @@ Result will be a long-lived access token: } """ +from __future__ import annotations + from datetime import timedelta -from typing import Union import uuid from aiohttp import web @@ -183,7 +184,7 @@ RESULT_TYPE_USER = "user" @bind_hass def create_auth_code( - hass, client_id: str, credential_or_user: Union[Credentials, User] + hass, client_id: str, credential_or_user: Credentials | User ) -> str: """Create an authorization code to fetch tokens.""" return hass.data[DOMAIN](client_id, credential_or_user) diff --git a/homeassistant/components/auth/translations/id.json b/homeassistant/components/auth/translations/id.json index f6a22386f99..ed7bede5fff 100644 --- a/homeassistant/components/auth/translations/id.json +++ b/homeassistant/components/auth/translations/id.json @@ -1,13 +1,32 @@ { "mfa_setup": { - "totp": { + "notify": { + "abort": { + "no_available_service": "Tidak ada layanan notifikasi yang tersedia." + }, "error": { - "invalid_code": "Kode salah, coba lagi. Jika Anda mendapatkan kesalahan ini secara konsisten, pastikan jam pada sistem Home Assistant anda akurat." + "invalid_code": "Kode tifak valid, coba lagi." }, "step": { "init": { - "description": "Untuk mengaktifkan otentikasi dua faktor menggunakan password satu kali berbasis waktu, pindai kode QR dengan aplikasi otentikasi Anda. Jika Anda tidak memilikinya, kami menyarankan [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \n Setelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi pengaturan. Jika Anda mengalami masalah saat memindai kode QR, lakukan pengaturan manual dengan kode ** ` {code} ` **.", - "title": "Siapkan otentikasi dua faktor menggunakan TOTP" + "description": "Pilih salah satu layanan notifikasi:", + "title": "Siapkan kata sandi sekali pakai yang dikirimkan oleh komponen notify" + }, + "setup": { + "description": "Kata sandi sekali pakai telah dikirim melalui **notify.{notify_service}**. Masukkan di bawah ini:", + "title": "Verifikasi penyiapan" + } + }, + "title": "Kata Sandi Sekali Pakai Notifikasi" + }, + "totp": { + "error": { + "invalid_code": "Kode tidak valid, coba lagi. Jika Anda terus mendapatkan kesalahan yang sama, pastikan jam pada sistem Home Assistant Anda sudah akurat." + }, + "step": { + "init": { + "description": "Untuk mengaktifkan autentikasi dua faktor menggunakan kata sandi sekali pakai berbasis waktu, pindai kode QR dengan aplikasi autentikasi Anda. Jika tidak punya, kami menyarankan aplikasi [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \nSetelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi penyiapan. Jika mengalami masalah saat memindai kode QR, lakukan penyiapan manual dengan kode **`{code}`**.", + "title": "Siapkan autentikasi dua faktor menggunakan TOTP" } }, "title": "TOTP" diff --git a/homeassistant/components/auth/translations/ko.json b/homeassistant/components/auth/translations/ko.json index 80850bb58b4..09af8eb89bf 100644 --- a/homeassistant/components/auth/translations/ko.json +++ b/homeassistant/components/auth/translations/ko.json @@ -5,7 +5,7 @@ "no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "error": { - "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "invalid_code": "\ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "step": { "init": { @@ -13,7 +13,7 @@ "title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815\ud558\uae30" }, "setup": { - "description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:", + "description": "**notify.{notify_service}**\uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574\uc8fc\uc138\uc694:", "title": "\uc124\uc815 \ud655\uc778\ud558\uae30" } }, @@ -21,11 +21,11 @@ }, "totp": { "error": { - "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." + "invalid_code": "\ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant\uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud558\uc5ec \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/)\ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud558\uc5ec \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131\ud558\uae30" } }, diff --git a/homeassistant/components/auth/translations/zh-Hant.json b/homeassistant/components/auth/translations/zh-Hant.json index 96e7f21ac99..8e769cb5983 100644 --- a/homeassistant/components/auth/translations/zh-Hant.json +++ b/homeassistant/components/auth/translations/zh-Hant.json @@ -25,8 +25,8 @@ }, "step": { "init": { - "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u96d9\u91cd\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", - "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u96d9\u91cd\u9a57\u8b49" + "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u96d9\u91cd\u8a8d\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", + "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u96d9\u91cd\u8a8d\u8b49" } }, "title": "TOTP" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7f006d929b1..36b7f1688f8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,6 +1,8 @@ """Allow to set up simple automation rules via the config file.""" +from __future__ import annotations + import logging -from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Union, cast +from typing import Any, Awaitable, Callable, Dict, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -8,6 +10,7 @@ from voluptuous.humanize import humanize_error from homeassistant.components import blueprint from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_NAME, CONF_ALIAS, CONF_CONDITION, @@ -46,21 +49,28 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import ( ATTR_CUR, ATTR_MAX, - ATTR_MODE, CONF_MAX, CONF_MAX_EXCEEDED, Script, ) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.trace import ( + TraceElement, + script_execution_set, + trace_append_element, + trace_get, + trace_path, +) from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime +from .config import AutomationConfig, async_validate_config_item + # Not used except by packages to check config structure -from .config import PLATFORM_SCHEMA # noqa -from .config import async_validate_config_item +from .config import PLATFORM_SCHEMA # noqa: F401 from .const import ( CONF_ACTION, CONF_INITIAL_STATE, @@ -71,6 +81,7 @@ from .const import ( LOGGER, ) from .helpers import async_get_blueprints +from .trace import trace_automation # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -90,6 +101,7 @@ ATTR_SOURCE = "source" ATTR_VARIABLES = "variables" SERVICE_TRIGGER = "trigger" +_LOGGER = logging.getLogger(__name__) AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] @@ -104,7 +116,7 @@ def is_on(hass, entity_id): @callback -def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: +def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all automations that reference the entity.""" if DOMAIN not in hass.data: return [] @@ -119,7 +131,7 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: @callback -def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: +def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all entities in a scene.""" if DOMAIN not in hass.data: return [] @@ -135,7 +147,7 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: @callback -def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: +def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]: """Return all automations that reference the device.""" if DOMAIN not in hass.data: return [] @@ -150,7 +162,7 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: @callback -def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: +def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all devices in a scene.""" if DOMAIN not in hass.data: return [] @@ -165,8 +177,40 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: return list(automation_entity.referenced_devices) +@callback +def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]: + """Return all automations that reference the area.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if area_id in automation_entity.referenced_areas + ] + + +@callback +def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all areas in an automation.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + automation_entity = component.get_entity(entity_id) + + if automation_entity is None: + return [] + + return list(automation_entity.referenced_areas) + + async def async_setup(hass, config): - """Set up the automation.""" + """Set up all automations.""" + # Local import to avoid circular import hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) # To register the automation blueprints @@ -176,9 +220,9 @@ async def async_setup(hass, config): await async_get_blueprints(hass).async_populate() async def trigger_service_handler(entity, service_call): - """Handle automation triggers.""" + """Handle forced automation trigger, e.g. from frontend.""" await entity.async_trigger( - service_call.data[ATTR_VARIABLES], + {**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}}, skip_condition=service_call.data[CONF_SKIP_CONDITION], context=service_call.context, ) @@ -228,6 +272,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): initial_state, variables, trigger_variables, + raw_config, + blueprint_inputs, ): """Initialize an automation entity.""" self._id = automation_id @@ -239,11 +285,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.action_script.change_listener = self.async_write_ha_state self._initial_state = initial_state self._is_enabled = False - self._referenced_entities: Optional[Set[str]] = None - self._referenced_devices: Optional[Set[str]] = None + self._referenced_entities: set[str] | None = None + self._referenced_devices: set[str] | None = None self._logger = LOGGER self._variables: ScriptVariables = variables self._trigger_variables: ScriptVariables = trigger_variables + self._raw_config = raw_config + self._blueprint_inputs = blueprint_inputs @property def name(self): @@ -261,7 +309,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return False @property - def state_attributes(self): + def extra_state_attributes(self): """Return the entity state attributes.""" attrs = { ATTR_LAST_TRIGGERED: self.action_script.last_triggered, @@ -270,6 +318,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): } if self.action_script.supports_max: attrs[ATTR_MAX] = self.action_script.max_runs + if self._id is not None: + attrs[CONF_ID] = self._id return attrs @property @@ -277,6 +327,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None or self._is_enabled + @property + def referenced_areas(self): + """Return a set of referenced areas.""" + return self.action_script.referenced_areas + @property def referenced_devices(self): """Return a set of referenced devices.""" @@ -374,52 +429,87 @@ class AutomationEntity(ToggleEntity, RestoreEntity): This method is a coroutine. """ - if self._variables: - try: - variables = self._variables.async_render(self.hass, run_variables) - except template.TemplateError as err: - self._logger.error("Error rendering variables: %s", err) - return - else: - variables = run_variables - - if ( - not skip_condition - and self._cond_func is not None - and not self._cond_func(variables) - ): - return + reason = "" + if "trigger" in run_variables and "description" in run_variables["trigger"]: + reason = f' by {run_variables["trigger"]["description"]}' + self._logger.debug("Automation triggered%s", reason) # Create a new context referring to the old context. parent_id = None if context is None else context.id trigger_context = Context(parent_id=parent_id) - self.async_set_context(trigger_context) - event_data = { - ATTR_NAME: self._name, - ATTR_ENTITY_ID: self.entity_id, - } - if "trigger" in variables and "description" in variables["trigger"]: - event_data[ATTR_SOURCE] = variables["trigger"]["description"] + with trace_automation( + self.hass, + self.unique_id, + self._raw_config, + self._blueprint_inputs, + trigger_context, + ) as automation_trace: + if self._variables: + try: + variables = self._variables.async_render(self.hass, run_variables) + except template.TemplateError as err: + self._logger.error("Error rendering variables: %s", err) + automation_trace.set_error(err) + return + else: + variables = run_variables + # Prepare tracing the automation + automation_trace.set_trace(trace_get()) - @callback - def started_action(): - self.hass.bus.async_fire( - EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context - ) + # Set trigger reason + trigger_description = variables.get("trigger", {}).get("description") + automation_trace.set_trigger_description(trigger_description) - try: - await self.action_script.async_run( - variables, trigger_context, started_action - ) - except (vol.Invalid, HomeAssistantError) as err: - self._logger.error( - "Error while executing automation %s: %s", - self.entity_id, - err, - ) - except Exception: # pylint: disable=broad-except - self._logger.exception("While executing automation %s", self.entity_id) + # Add initial variables as the trigger step + if "trigger" in variables and "id" in variables["trigger"]: + trigger_path = f"trigger/{variables['trigger']['id']}" + else: + trigger_path = "trigger" + trace_element = TraceElement(variables, trigger_path) + trace_append_element(trace_element) + + if ( + not skip_condition + and self._cond_func is not None + and not self._cond_func(variables) + ): + self._logger.debug( + "Conditions not met, aborting automation. Condition summary: %s", + trace_get(clear=False), + ) + script_execution_set("failed_conditions") + return + + self.async_set_context(trigger_context) + event_data = { + ATTR_NAME: self._name, + ATTR_ENTITY_ID: self.entity_id, + } + if "trigger" in variables and "description" in variables["trigger"]: + event_data[ATTR_SOURCE] = variables["trigger"]["description"] + + @callback + def started_action(): + self.hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context + ) + + try: + with trace_path("action"): + await self.action_script.async_run( + variables, trigger_context, started_action + ) + except (vol.Invalid, HomeAssistantError) as err: + self._logger.error( + "Error while executing automation %s: %s", + self.entity_id, + err, + ) + automation_trace.set_error(err) + except Exception as err: # pylint: disable=broad-except + self._logger.exception("While executing automation %s", self.entity_id) + automation_trace.set_error(err) async def async_will_remove_from_hass(self): """Remove listeners when removing automation from Home Assistant.""" @@ -473,7 +563,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): async def _async_attach_triggers( self, home_assistant_start: bool - ) -> Optional[Callable[[], None]]: + ) -> Callable[[], None] | None: """Set up the triggers.""" def log_cb(level, msg, **kwargs): @@ -483,14 +573,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity): if self._trigger_variables: try: variables = self._trigger_variables.async_render( - cast(HomeAssistant, self.hass), None, limited=True + self.hass, None, limited=True ) except template.TemplateError as err: self._logger.error("Error rendering trigger variables: %s", err) return None return await async_initialize_triggers( - cast(HomeAssistant, self.hass), + self.hass, self._trigger_config, self.async_trigger, DOMAIN, @@ -500,18 +590,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity): variables, ) - @property - def device_state_attributes(self): - """Return automation attributes.""" - if self._id is None: - return None - - return {CONF_ID: self._id} - async def _async_process_config( hass: HomeAssistant, - config: Dict[str, Any], + config: dict[str, Any], component: EntityComponent, ) -> bool: """Process config and add automations. @@ -522,21 +604,23 @@ async def _async_process_config( blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): - conf: List[Union[Dict[str, Any], blueprint.BlueprintInputs]] = config[ # type: ignore + conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[ # type: ignore 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 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, blueprint_inputs.async_substitute() - ), + await async_validate_config_item(hass, raw_config), ) except vol.Invalid as err: LOGGER.error( @@ -546,6 +630,8 @@ async def _async_process_config( humanize_error(config_block, err), ) continue + else: + raw_config = cast(AutomationConfig, config_block).raw_config automation_id = config_block.get(CONF_ID) name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" @@ -596,6 +682,8 @@ async def _async_process_config( initial_state, variables, config_block.get(CONF_TRIGGER_VARIABLES), + raw_config, + raw_blueprint_inputs, ) entities.append(entity) @@ -623,8 +711,9 @@ async def _async_process_if(hass, name, config, p_config): errors = [] for index, check in enumerate(checks): try: - if not check(hass, variables): - return False + with trace_path(["condition", str(index)]): + if not check(hass, variables): + return False except ConditionError as ex: errors.append( ConditionErrorIndex( @@ -648,7 +737,7 @@ async def _async_process_if(hass, name, config, p_config): @callback -def _trigger_extract_device(trigger_conf: dict) -> Optional[str]: +def _trigger_extract_device(trigger_conf: dict) -> str | None: """Extract devices from a trigger config.""" if trigger_conf[CONF_PLATFORM] != "device": return None @@ -657,7 +746,7 @@ def _trigger_extract_device(trigger_conf: dict) -> Optional[str]: @callback -def _trigger_extract_entities(trigger_conf: dict) -> List[str]: +def _trigger_extract_entities(trigger_conf: dict) -> list[str]: """Extract entities from a trigger config.""" if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): return trigger_conf[CONF_ENTITY_ID] diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml index c11d22d974e..54a4a4f0643 100644 --- a/homeassistant/components/automation/blueprints/motion_light.yaml +++ b/homeassistant/components/automation/blueprints/motion_light.yaml @@ -38,13 +38,17 @@ trigger: to: "on" action: - - service: light.turn_on + - alias: "Turn on the light" + service: light.turn_on target: !input light_target - - wait_for_trigger: + - alias: "Wait until there is no motion from device" + wait_for_trigger: platform: state entity_id: !input motion_entity from: "on" to: "off" - - delay: !input no_motion_wait - - service: light.turn_off + - alias: "Wait the number of seconds that has been set" + delay: !input no_motion_wait + - alias: "Turn off the light" + service: light.turn_off target: !input light_target diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml index d3a70d773ee..71abf8f865c 100644 --- a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml +++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml @@ -37,7 +37,8 @@ condition: value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" action: - domain: mobile_app - type: notify - device_id: !input notify_device - message: "{{ person_name }} has left {{ zone_state }}" + - alias: "Notify that a person has left the zone" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 32ad92cb86e..b4b8b49fa3e 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -1,5 +1,6 @@ """Config validation helper for the automation integration.""" import asyncio +from contextlib import suppress import voluptuous as vol @@ -8,7 +9,13 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.config import async_log_exception, config_without_domain -from homeassistant.const import CONF_ALIAS, CONF_CONDITION, CONF_ID, CONF_VARIABLES +from homeassistant.const import ( + CONF_ALIAS, + CONF_CONDITION, + CONF_DESCRIPTION, + CONF_ID, + CONF_VARIABLES, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, config_validation as cv, script from homeassistant.helpers.condition import async_validate_condition_config @@ -17,7 +24,6 @@ from homeassistant.loader import IntegrationNotFound from .const import ( CONF_ACTION, - CONF_DESCRIPTION, CONF_HIDE_ENTITY, CONF_INITIAL_STATE, CONF_TRIGGER, @@ -79,8 +85,18 @@ async def async_validate_config_item(hass, config, full_config=None): return config +class AutomationConfig(dict): + """Dummy class to allow adding attributes.""" + + raw_config = None + + async def _try_async_validate_config_item(hass, config, full_config=None): """Validate config item.""" + raw_config = None + with suppress(ValueError): + raw_config = dict(config) + try: config = await async_validate_config_item(hass, config, full_config) except ( @@ -92,6 +108,11 @@ async def _try_async_validate_config_item(hass, config, full_config=None): async_log_exception(ex, DOMAIN, full_config or config, hass) return None + if isinstance(config, blueprint.BlueprintInputs): + return config + + config = AutomationConfig(config) + config.raw_config = raw_config return config diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index 829f78590e0..d6f34ddfeb6 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -6,7 +6,6 @@ CONF_TRIGGER = "trigger" CONF_TRIGGER_VARIABLES = "trigger_variables" DOMAIN = "automation" -CONF_DESCRIPTION = "description" CONF_HIDE_ENTITY = "hide_entity" CONF_CONDITION_TYPE = "condition_type" diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 3c9671af18f..b5dbada1b7a 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,26 +1,29 @@ """Describe logbook events.""" +from homeassistant.components.logbook import LazyEventPartialState from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from . import ATTR_SOURCE, DOMAIN, EVENT_AUTOMATION_TRIGGERED @callback -def async_describe_events(hass, async_describe_event): # type: ignore +def async_describe_events(hass: HomeAssistant, async_describe_event): # type: ignore """Describe logbook events.""" @callback - def async_describe_logbook_event(event): # type: ignore + def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore """Describe a logbook event.""" data = event.data message = "has been triggered" if ATTR_SOURCE in data: message = f"{message} by {data[ATTR_SOURCE]}" + return { "name": data.get(ATTR_NAME), "message": message, "source": data.get(ATTR_SOURCE), "entity_id": data.get(ATTR_ENTITY_ID), + "context_id": event.context_id, } async_describe_event( diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 2db56eb597f..2483f57de8e 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -2,7 +2,7 @@ "domain": "automation", "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", - "dependencies": ["blueprint"], + "dependencies": ["blueprint", "trace"], "after_dependencies": [ "device_automation", "webhook" diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index bcd0cc4e585..ff716f3a83b 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Automation state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -10,8 +12,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 @@ -21,11 +22,11 @@ VALID_STATES = {STATE_ON, STATE_OFF} async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -57,11 +58,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Automation states.""" await asyncio.gather( diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py new file mode 100644 index 00000000000..cfdbe02056b --- /dev/null +++ b/homeassistant/components/automation/trace.py @@ -0,0 +1,54 @@ +"""Trace support for automation.""" +from __future__ import annotations + +from contextlib import contextmanager +from typing import Any + +from homeassistant.components.trace import ActionTrace, async_store_trace +from homeassistant.core import Context + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + + +class AutomationTrace(ActionTrace): + """Container for automation trace.""" + + def __init__( + self, + item_id: str, + config: dict[str, Any], + blueprint_inputs: dict[str, Any], + context: Context, + ): + """Container for automation trace.""" + key = ("automation", item_id) + super().__init__(key, config, blueprint_inputs, context) + self._trigger_description: str | None = None + + def set_trigger_description(self, trigger: str) -> None: + """Set trigger description.""" + self._trigger_description = trigger + + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this AutomationTrace.""" + result = super().as_short_dict() + result["trigger"] = self._trigger_description + return result + + +@contextmanager +def trace_automation(hass, automation_id, config, blueprint_inputs, context): + """Trace action execution of automation with automation_id.""" + trace = AutomationTrace(automation_id, config, blueprint_inputs, context) + async_store_trace(hass, trace) + + try: + yield trace + except Exception as ex: + if automation_id: + trace.set_error(ex) + raise ex + finally: + if automation_id: + trace.finished() diff --git a/homeassistant/components/automation/translations/id.json b/homeassistant/components/automation/translations/id.json index eabfe0b64aa..58e8497c8b9 100644 --- a/homeassistant/components/automation/translations/id.json +++ b/homeassistant/components/automation/translations/id.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Otomasi" diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 0af2f15b34e..0d242b952dd 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an Avion switch.""" - # pylint: disable=no-member avion = importlib.import_module("avion") lights = [] @@ -111,7 +110,6 @@ class AvionLight(LightEntity): def set_state(self, brightness): """Set the state of this lamp to the provided brightness.""" - # pylint: disable=no-member avion = importlib.import_module("avion") # Bluetooth LE is unreliable, and the connection may drop at any diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 56af5d2b662..bfb95fd91fc 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -1,15 +1,15 @@ """The awair component.""" +from __future__ import annotations from asyncio import gather -from typing import Any, Optional +from typing import Any 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.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,20 +18,12 @@ from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL, AwairResult PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up Awair integration.""" - return True - - async def async_setup_entry(hass, config_entry) -> bool: """Set up Awair integration from a config entry.""" session = async_get_clientsession(hass) coordinator = AwairDataUpdateCoordinator(hass, config_entry, session) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator @@ -70,7 +62,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - async def _async_update_data(self) -> Optional[Any]: + async def _async_update_data(self) -> Any | None: """Update data via Awair client library.""" with timeout(API_TIMEOUT): try: @@ -83,7 +75,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): return {result.device.uuid: result for result in results} except AuthError as err: flow_context = { - "source": "reauth", + "source": SOURCE_REAUTH, "unique_id": self._config_entry.unique_id, } diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index a4768014f96..76c7cbca3a9 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -1,6 +1,5 @@ """Config flow for Awair.""" - -from typing import Optional +from __future__ import annotations from python_awair import Awair from python_awair.exceptions import AuthError, AwairError @@ -10,7 +9,7 @@ from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, LOGGER # pylint: disable=unused-import +from .const import DOMAIN, LOGGER class AwairFlowHandler(ConfigFlow, domain=DOMAIN): @@ -36,7 +35,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): data={CONF_ACCESS_TOKEN: conf[CONF_ACCESS_TOKEN]}, ) - async def async_step_user(self, user_input: Optional[dict] = None): + async def async_step_user(self, user_input: dict | None = None): """Handle a flow initialized by the user.""" errors = {} @@ -61,7 +60,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input: Optional[dict] = None): + async def async_step_reauth(self, user_input: dict | None = None): """Handle re-auth if token invalid.""" errors = {} diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 44490b8401f..2853ef9dd6c 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, @@ -104,7 +105,7 @@ SENSOR_TYPES = { ATTR_UNIQUE_ID: "PM10", # matches legacy format }, API_CO2: { - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_CO2, ATTR_ICON: "mdi:cloud", ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, ATTR_LABEL: "Carbon dioxide", diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 421fa3d8a26..502fa3dc626 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -1,12 +1,13 @@ """Support for Awair sensors.""" +from __future__ import annotations -from typing import Callable, List, Optional +from typing import Callable from python_awair.devices import AwairDevice import voluptuous as vol from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult -from homeassistant.components.sensor import PLATFORM_SCHEMA +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.helpers import device_registry as dr @@ -41,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Import Awair configuration from YAML.""" LOGGER.warning( - "Loading Awair via platform setup is deprecated. Please remove it from your configuration." + "Loading Awair via platform setup is deprecated; Please remove it from your configuration" ) hass.async_create_task( hass.config_entries.flow.async_init( @@ -55,13 +56,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigType, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ): """Set up Awair sensor entity based on a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] sensors = [] - data: List[AwairResult] = coordinator.data.values() + data: list[AwairResult] = coordinator.data.values() for result in data: if result.air_data: sensors.append(AwairSensor(API_SCORE, result.device, coordinator)) @@ -83,7 +84,7 @@ async def async_setup_entry( async_add_entities(sensors) -class AwairSensor(CoordinatorEntity): +class AwairSensor(CoordinatorEntity, SensorEntity): """Defines an Awair sensor entity.""" def __init__( @@ -179,7 +180,7 @@ class AwairSensor(CoordinatorEntity): return SENSOR_TYPES[self._kind][ATTR_UNIT] @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the Awair Index alongside state attributes. The Awair Index is a subjective score ranging from 0-4 (inclusive) that @@ -228,9 +229,9 @@ class AwairSensor(CoordinatorEntity): return info @property - def _air_data(self) -> Optional[AwairResult]: + def _air_data(self) -> AwairResult | None: """Return the latest data for our device, or None.""" - result: Optional[AwairResult] = self.coordinator.data.get(self._device.uuid) + result: AwairResult | None = self.coordinator.data.get(self._device.uuid) if result: return result.air_data diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 436e8b1fb7d..53827adf344 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + }, + "description": "Add meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." + }, + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json new file mode 100644 index 00000000000..2c6fab90909 --- /dev/null +++ b/homeassistant/components/awair/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_access_token": "Token akses tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Masukkan kembali token akses pengembang Awair Anda." + }, + "user": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 5d20aed2fdb..d41b85cc09b 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -21,7 +21,8 @@ "data": { "access_token": "Toegangstoken", "email": "E-mail" - } + }, + "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 8722c41c3e0..378d02bcccd 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -13,11 +13,6 @@ from .device import AxisNetworkDevice _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Old way to set up Axis devices.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up the Axis component.""" hass.data.setdefault(AXIS_DOMAIN, {}) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index bd7b5e442ad..f732ad2fb5d 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -304,13 +304,13 @@ async def get_device(hass, host, port, username, password): ) try: - with async_timeout.timeout(15): + with async_timeout.timeout(30): await device.vapix.initialize() return device except axis.Unauthorized as err: - LOGGER.warning("Connected to device at %s but not registered.", host) + LOGGER.warning("Connected to device at %s but not registered", host) raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index a78d916da9e..b709ac35da2 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==43"], + "requirements": ["axis==44"], "dhcp": [ { "hostname": "axis-00408c*", "macaddress": "00408C*" }, { "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" }, diff --git a/homeassistant/components/axis/translations/he.json b/homeassistant/components/axis/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/axis/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index 659c50e49e7..972690ede97 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "flow_title": "Axis eszk\u00f6z: {name} ({host})", "step": { diff --git a/homeassistant/components/axis/translations/id.json b/homeassistant/components/axis/translations/id.json new file mode 100644 index 00000000000..cdd498a8e6c --- /dev/null +++ b/homeassistant/components/axis/translations/id.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "link_local_address": "Tautan alamat lokal tidak didukung", + "not_axis_device": "Perangkat yang ditemukan bukan perangkat Axis" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "title": "Siapkan perangkat Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Pilih profil streaming yang akan digunakan" + }, + "title": "Opsi streaming video perangkat Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/nl.json b/homeassistant/components/axis/translations/nl.json index 345e6622e93..3b41c1184ba 100644 --- a/homeassistant/components/axis/translations/nl.json +++ b/homeassistant/components/axis/translations/nl.json @@ -7,11 +7,11 @@ }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.", + "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "Axis apparaat: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -23,5 +23,15 @@ "title": "Stel het Axis-apparaat in" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecteer stream profiel om te gebruiken" + }, + "title": "Opties voor videostreams van Axis-apparaten" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json index 1bf3e369b65..f5c5e79a32f 100644 --- a/homeassistant/components/axis/translations/ru.json +++ b/homeassistant/components/axis/translations/ru.json @@ -18,7 +18,7 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Axis" } diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index f72a4c44918..3db74679d9a 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -1,6 +1,8 @@ """Support for Azure DevOps.""" +from __future__ import annotations + import logging -from typing import Any, Dict +from typing import Any from aioazuredevops.client import DevOpsClient import aiohttp @@ -12,7 +14,7 @@ from homeassistant.components.azure_devops.const import ( DATA_AZURE_DEVOPS_CLIENT, DOMAIN, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -20,11 +22,6 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the Azure DevOps components.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Azure DevOps from a config entry.""" client = DevOpsClient() @@ -39,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=entry.data, ) ) @@ -100,7 +97,7 @@ class AzureDevOpsEntity(Entity): else: if self._available: _LOGGER.debug( - "An error occurred while updating Azure DevOps sensor.", + "An error occurred while updating Azure DevOps sensor", exc_info=True, ) self._available = False @@ -114,7 +111,7 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): """Defines a Azure DevOps device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this Azure DevOps instance.""" return { "identifiers": { diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index d7d6f2868c3..138ea67e788 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -4,7 +4,7 @@ import aiohttp import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.azure_devops.const import ( # pylint:disable=unused-import +from homeassistant.components.azure_devops.const import ( CONF_ORG, CONF_PAT, CONF_PROJECT, diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 6f259afb9a9..01018d34c78 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -1,7 +1,8 @@ """Support for Azure DevOps sensors.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import List from aioazuredevops.builds import DevOpsBuild from aioazuredevops.client import DevOpsClient @@ -16,6 +17,7 @@ from homeassistant.components.azure_devops.const import ( DATA_PROJECT, DOMAIN, ) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType @@ -39,7 +41,7 @@ async def async_setup_entry( sensors = [] try: - builds: List[DevOpsBuild] = await client.get_builds( + builds: list[DevOpsBuild] = await client.get_builds( organization, project, BUILDS_QUERY ) except aiohttp.ClientError as exception: @@ -54,7 +56,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class AzureDevOpsSensor(AzureDevOpsDeviceEntity): +class AzureDevOpsSensor(AzureDevOpsDeviceEntity, SensorEntity): """Defines a Azure DevOps sensor.""" def __init__( @@ -92,7 +94,7 @@ class AzureDevOpsSensor(AzureDevOpsDeviceEntity): return self._state @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return the attributes of the sensor.""" return self._attributes diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index 6bd42409877..460b6132048 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -1,11 +1,19 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "reauth": { "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait." + }, + "user": { + "title": "Azure DevOps Project hozz\u00e1ad\u00e1sa" } } } diff --git a/homeassistant/components/azure_devops/translations/id.json b/homeassistant/components/azure_devops/translations/id.json new file mode 100644 index 00000000000..42292805b08 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "project_error": "Tidak bisa mendapatkan info proyek." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "Token Akses Pribadi (PAT)" + }, + "description": "Autentikasi gagal untuk {project_url} . Masukkan kredensial Anda saat ini.", + "title": "Autentikasi ulang" + }, + "user": { + "data": { + "organization": "Organisasi", + "personal_access_token": "Token Akses Pribadi (PAT)", + "project": "Proyek" + }, + "description": "Siapkan instans Azure DevOps untuk mengakses proyek Anda. Token Akses Pribadi hanya diperlukan untuk proyek pribadi.", + "title": "Tambahkan Proyek Azure DevOps" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/ko.json b/homeassistant/components/azure_devops/translations/ko.json index 555c548a142..cdb67cf77df 100644 --- a/homeassistant/components/azure_devops/translations/ko.json +++ b/homeassistant/components/azure_devops/translations/ko.json @@ -6,7 +6,27 @@ }, "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_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "project_error": "\ud504\ub85c\uc81d\ud2b8 \uc815\ubcf4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "data": { + "personal_access_token": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 (PAT)" + }, + "description": "{project_url} \uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\uc7ac\uc778\uc99d" + }, + "user": { + "data": { + "organization": "\uc870\uc9c1", + "personal_access_token": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 (PAT)", + "project": "\ud504\ub85c\uc81d\ud2b8" + }, + "description": "\ud504\ub85c\uc81d\ud2b8\uc5d0 \uc811\uadfc\ud560 Azure DevOps \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070\uc740 \uac1c\uc778 \ud504\ub85c\uc81d\ud2b8\uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4.", + "title": "Azure DevOps \ud504\ub85c\uc81d\ud2b8 \ucd94\uac00\ud558\uae30" + } } } } \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/nl.json b/homeassistant/components/azure_devops/translations/nl.json index 07dc59e1a56..971af5b8d58 100644 --- a/homeassistant/components/azure_devops/translations/nl.json +++ b/homeassistant/components/azure_devops/translations/nl.json @@ -9,12 +9,23 @@ "invalid_auth": "Ongeldige authenticatie", "project_error": "Kon geen projectinformatie ophalen." }, + "flow_title": "Azure DevOps: {project_url}", "step": { + "reauth": { + "data": { + "personal_access_token": "Persoonlijk toegangstoken (PAT)" + }, + "description": "Authenticatie mislukt voor {project_url}. Voer uw huidige inloggegevens in.", + "title": "Herauthenticatie" + }, "user": { "data": { "organization": "Organisatie", + "personal_access_token": "Persoonlijk toegangstoken (PAT)", "project": "Project" - } + }, + "description": "Stel een Azure DevOps instantie in om toegang te krijgen tot uw project. Een persoonlijke toegangstoken is alleen nodig voor een priv\u00e9project.", + "title": "Azure DevOps-project toevoegen" } } } diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 3b44c6423be..0473c4ff5a7 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -1,9 +1,11 @@ """Support for Azure Event Hubs.""" +from __future__ import annotations + import asyncio import json import logging import time -from typing import Any, Dict +from typing import Any from azure.eventhub import EventData from azure.eventhub.aio import EventHubProducerClient, EventHubSharedKeyCredential @@ -95,7 +97,7 @@ class AzureEventHub: def __init__( self, hass: HomeAssistant, - client_args: Dict[str, Any], + client_args: dict[str, Any], conn_str_client: bool, entities_filter: vol.Schema, send_interval: int, diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 69553e921eb..6879e278bab 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -384,7 +384,7 @@ class BayesianBinarySensor(BinarySensorEntity): return self._device_class @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" attr_observations_list = [ diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index 61d98c7413e..f7d146e073e 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -8,7 +8,6 @@ DOMAIN = "bbb_gpio" def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" - # pylint: disable=import-error def cleanup_gpio(event): """Stuff to do before stopping.""" diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 8097c11eb89..9dac635dd2f 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -1,8 +1,9 @@ """Support for French FAI Bouygues Bbox routers.""" +from __future__ import annotations + from collections import namedtuple from datetime import timedelta import logging -from typing import List import pybbox import voluptuous as vol @@ -47,7 +48,7 @@ class BboxDeviceScanner(DeviceScanner): self.host = config[CONF_HOST] """Initialize the scanner.""" - self.last_results: List[Device] = [] + self.last_results: list[Device] = [] self.success_init = self._update_info() _LOGGER.info("Scanner initialized") @@ -74,7 +75,7 @@ class BboxDeviceScanner(DeviceScanner): Returns boolean if scanning successful. """ - _LOGGER.info("Scanning...") + _LOGGER.info("Scanning") box = pybbox.Bbox(ip=self.host) result = box.get_all_connected_devices() diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 13c8f5bb03f..5256c2a61a0 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -6,7 +6,7 @@ import pybbox import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_MONITORED_VARIABLES, @@ -15,7 +15,6 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import utcnow @@ -86,7 +85,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class BboxUptimeSensor(Entity): +class BboxUptimeSensor(SensorEntity): """Bbox uptime sensor.""" def __init__(self, bbox_data, sensor_type, name): @@ -115,7 +114,7 @@ class BboxUptimeSensor(Entity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -133,7 +132,7 @@ class BboxUptimeSensor(Entity): self._state = uptime.replace(microsecond=0).isoformat() -class BboxSensor(Entity): +class BboxSensor(SensorEntity): """Implementation of a Bbox sensor.""" def __init__(self, bbox_data, sensor_type, name): @@ -167,7 +166,7 @@ class BboxSensor(Entity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index e8a37d51be4..9bf935f3c4f 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -2,7 +2,7 @@ from beewi_smartclim import BeewiSmartClimPoller # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MAC, CONF_NAME, @@ -13,7 +13,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity # Default values DEFAULT_NAME = "BeeWi SmartClim" @@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class BeewiSmartclimSensor(Entity): +class BeewiSmartclimSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, poller, name, mac, device, unit): diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 7680b8b09ad..5b708ae2630 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -3,13 +3,12 @@ from functools import partial import logging from i2csense.bh1750 import BH1750 # pylint: disable=import-error -import smbus # pylint: disable=import-error +import smbus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -94,7 +93,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class BH1750Sensor(Entity): +class BH1750Sensor(SensorEntity): """Implementation of the BH1750 sensor.""" def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 999a62b3a80..8c506634200 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,5 +1,5 @@ """Implement device conditions for binary sensor.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -205,9 +205,9 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions.""" - conditions: List[Dict[str, str]] = [] + conditions: list[dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry diff --git a/homeassistant/components/binary_sensor/group.py b/homeassistant/components/binary_sensor/group.py index 1636054663d..234883ffd5a 100644 --- a/homeassistant/components/binary_sensor/group.py +++ b/homeassistant/components/binary_sensor/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/binary_sensor/significant_change.py b/homeassistant/components/binary_sensor/significant_change.py index bc2dba04f09..8421483ba0c 100644 --- a/homeassistant/components/binary_sensor/significant_change.py +++ b/homeassistant/components/binary_sensor/significant_change.py @@ -1,5 +1,7 @@ """Helper to test significant Binary Sensor state changes.""" -from typing import Any, Optional +from __future__ import annotations + +from typing import Any from homeassistant.core import HomeAssistant, callback @@ -12,7 +14,7 @@ def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs: Any, -) -> Optional[bool]: +) -> bool | None: """Test if state significantly changed.""" if old_state != new_state: return True diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index 4ca757da6e5..ac880aa28fa 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -1,13 +1,107 @@ { + "device_automation": { + "condition_type": { + "is_bat_low": "Baterai {entity_name} hampir habis", + "is_cold": "{entity_name} dingin", + "is_connected": "{entity_name} terhubung", + "is_gas": "{entity_name} mendeteksi gas", + "is_hot": "{entity_name} panas", + "is_light": "{entity_name} mendeteksi cahaya", + "is_locked": "{entity_name} terkunci", + "is_moist": "{entity_name} lembab", + "is_motion": "{entity_name} mendeteksi gerakan", + "is_moving": "{entity_name} bergerak", + "is_no_gas": "{entity_name} tidak mendeteksi gas", + "is_no_light": "{entity_name} tidak mendeteksi cahaya", + "is_no_motion": "{entity_name} tidak mendeteksi gerakan", + "is_no_problem": "{entity_name} tidak mendeteksi masalah", + "is_no_smoke": "{entity_name} tidak mendeteksi asap", + "is_no_sound": "{entity_name} tidak mendeteksi suara", + "is_no_vibration": "{entity_name} tidak mendeteksi getaran", + "is_not_bat_low": "Baterai {entity_name} normal", + "is_not_cold": "{entity_name} tidak dingin", + "is_not_connected": "{entity_name} terputus", + "is_not_hot": "{entity_name} tidak panas", + "is_not_locked": "{entity_name} tidak terkunci", + "is_not_moist": "{entity_name} kering", + "is_not_moving": "{entity_name} tidak bergerak", + "is_not_occupied": "{entity_name} tidak ditempati", + "is_not_open": "{entity_name} tertutup", + "is_not_plugged_in": "{entity_name} dicabut", + "is_not_powered": "{entity_name} tidak ditenagai", + "is_not_present": "{entity_name} tidak ada", + "is_not_unsafe": "{entity_name} aman", + "is_occupied": "{entity_name} ditempati", + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala", + "is_open": "{entity_name} terbuka", + "is_plugged_in": "{entity_name} dicolokkan", + "is_powered": "{entity_name} ditenagai", + "is_present": "{entity_name} ada", + "is_problem": "{entity_name} mendeteksi masalah", + "is_smoke": "{entity_name} mendeteksi asap", + "is_sound": "{entity_name} mendeteksi suara", + "is_unsafe": "{entity_name} tidak aman", + "is_vibration": "{entity_name} mendeteksi getaran" + }, + "trigger_type": { + "bat_low": "Baterai {entity_name} hampir habis", + "cold": "{entity_name} menjadi dingin", + "connected": "{entity_name} terhubung", + "gas": "{entity_name} mulai mendeteksi gas", + "hot": "{entity_name} menjadi panas", + "light": "{entity_name} mulai mendeteksi cahaya", + "locked": "{entity_name} terkunci", + "moist": "{entity_name} menjadi lembab", + "motion": "{entity_name} mulai mendeteksi gerakan", + "moving": "{entity_name} mulai bergerak", + "no_gas": "{entity_name} berhenti mendeteksi gas", + "no_light": "{entity_name} berhenti mendeteksi cahaya", + "no_motion": "{entity_name} berhenti mendeteksi gerakan", + "no_problem": "{entity_name} berhenti mendeteksi masalah", + "no_smoke": "{entity_name} berhenti mendeteksi asap", + "no_sound": "{entity_name} berhenti mendeteksi suara", + "no_vibration": "{entity_name} berhenti mendeteksi getaran", + "not_bat_low": "Baterai {entity_name} normal", + "not_cold": "{entity_name} menjadi tidak dingin", + "not_connected": "{entity_name} terputus", + "not_hot": "{entity_name} menjadi tidak panas", + "not_locked": "{entity_name} tidak terkunci", + "not_moist": "{entity_name} menjadi kering", + "not_moving": "{entity_name} berhenti bergerak", + "not_occupied": "{entity_name} menjadi tidak ditempati", + "not_opened": "{entity_name} tertutup", + "not_plugged_in": "{entity_name} dicabut", + "not_powered": "{entity_name} tidak ditenagai", + "not_present": "{entity_name} tidak ada", + "not_unsafe": "{entity_name} menjadi aman", + "occupied": "{entity_name} menjadi ditempati", + "opened": "{entity_name} terbuka", + "plugged_in": "{entity_name} dicolokkan", + "powered": "{entity_name} ditenagai", + "present": "{entity_name} ada", + "problem": "{entity_name} mulai mendeteksi masalah", + "smoke": "{entity_name} mulai mendeteksi asap", + "sound": "{entity_name} mulai mendeteksi suara", + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan", + "unsafe": "{entity_name} menjadi tidak aman", + "vibration": "{entity_name} mulai mendeteksi getaran" + } + }, "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" }, "battery": { "off": "Normal", "on": "Rendah" }, + "battery_charging": { + "off": "Tidak mengisi daya", + "on": "Mengisi daya" + }, "cold": { "off": "Normal", "on": "Dingin" @@ -25,13 +119,17 @@ "on": "Terbuka" }, "gas": { - "off": "Kosong", + "off": "Tidak ada", "on": "Terdeteksi" }, "heat": { "off": "Normal", "on": "Panas" }, + "light": { + "off": "Tidak ada cahaya", + "on": "Cahaya terdeteksi" + }, "lock": { "off": "Terkunci", "on": "Terbuka" @@ -44,6 +142,10 @@ "off": "Tidak ada", "on": "Terdeteksi" }, + "moving": { + "off": "Tidak bergerak", + "on": "Bergerak" + }, "occupancy": { "off": "Tidak ada", "on": "Terdeteksi" @@ -52,13 +154,17 @@ "off": "Tertutup", "on": "Terbuka" }, + "plug": { + "off": "Dicabut", + "on": "Dicolokkan" + }, "presence": { "off": "Keluar", - "on": "Rumah" + "on": "Di Rumah" }, "problem": { "off": "Oke", - "on": "Masalah" + "on": "Bermasalah" }, "safety": { "off": "Aman", diff --git a/homeassistant/components/binary_sensor/translations/ko.json b/homeassistant/components/binary_sensor/translations/ko.json index 0b8ef0b73d5..7a725fc6719 100644 --- a/homeassistant/components/binary_sensor/translations/ko.json +++ b/homeassistant/components/binary_sensor/translations/ko.json @@ -1,92 +1,92 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", - "is_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc73c\uba74", - "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74", - "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uba74", - "is_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc73c\uba74", - "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uba74", - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", - "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud558\uba74", - "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uba74", - "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uba74", - "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74", - "is_not_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uc73c\uba74", - "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\ub2e4\uba74", - "is_not_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uc73c\uba74", - "is_not_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", - "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud558\uba74", - "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc73c\uba74", - "is_not_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uba74", - "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", - "is_not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74", - "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc73c\uba74", - "is_not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74", - "is_not_unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uba74", - "is_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uc774\uba74", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", - "is_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74", - "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74", - "is_present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74", - "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uba74", - "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uba74", - "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uba74", - "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74", - "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uba74" + "is_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", + "is_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc73c\uba74", + "is_connected": "{entity_name}\uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc73c\uba74", + "is_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_moist": "{entity_name}\uc774(\uac00) \uc2b5\ud558\uba74", + "is_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uace0 \uc788\uc73c\uba74", + "is_no_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_no_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74", + "is_not_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uc73c\uba74", + "is_not_connected": "{entity_name}\uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\uc73c\uba74", + "is_not_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uc73c\uba74", + "is_not_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_moist": "{entity_name}\uc774(\uac00) \uac74\uc870\ud558\uba74", + "is_not_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uba74", + "is_not_open": "{entity_name}\uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_not_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74", + "is_not_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_present": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74", + "is_not_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uba74", + "is_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uc774\uba74", + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_open": "{entity_name}\uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74", + "is_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74", + "is_present": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74", + "is_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74", + "is_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uace0 \uc788\uc73c\uba74" }, "trigger_type": { - "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c", - "cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc544\uc84c\uc744 \ub54c", - "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub420 \ub54c", - "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud560 \ub54c", - "hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc544\uc84c\uc744 \ub54c", - "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud560 \ub54c", - "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", - "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", - "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud560 \ub54c", - "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc77c \ub54c", - "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", - "not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub420 \ub54c", - "not_cold": "{entity_name} \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uac8c \ub410\uc744 \ub54c", - "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9c8 \ub54c", - "not_hot": "{entity_name} \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uac8c \ub410\uc744 \ub54c", - "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c", - "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9c8 \ub54c", - "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc744 \ub54c", - "not_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uac8c \ub420 \ub54c", - "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", - "not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud790 \ub54c", - "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc744 \ub54c", - "not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub420 \ub54c", - "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9c8 \ub54c", - "occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", - "plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud790 \ub54c", - "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub420 \ub54c", - "present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c", - "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud560 \ub54c", - "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud560 \ub54c", - "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud560 \ub54c", - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c", - "unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc744 \ub54c", - "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud560 \ub54c" + "bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc84c\uc744 \ub54c", + "cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc544\uc84c\uc744 \ub54c", + "connected": "{entity_name}\uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc744 \ub54c", + "gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc544\uc84c\uc744 \ub54c", + "light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "locked": "{entity_name}\uc774(\uac00) \uc7a0\uacbc\uc744 \ub54c", + "moist": "{entity_name}\uc774(\uac00) \uc2b5\ud574\uc84c\uc744 \ub54c", + "motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "no_gas": "{entity_name}\uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_light": "{entity_name}\uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_motion": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "no_vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_bat_low": "{entity_name}\uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub418\uc5c8\uc744 \ub54c", + "not_cold": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub0ae\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_connected": "{entity_name}\uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc744 \ub54c", + "not_hot": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ub192\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_locked": "{entity_name}\uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c", + "not_moist": "{entity_name}\uc774(\uac00) \uac74\uc870\ud574\uc84c\uc744 \ub54c", + "not_moving": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_opened": "{entity_name}\uc774(\uac00) \ub2eb\ud614\uc744 \ub54c", + "not_plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \ubf51\ud614\uc744 \ub54c", + "not_powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "not_present": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c", + "not_unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud574\uc84c\uc744 \ub54c", + "occupied": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c", + "opened": "{entity_name}\uc774(\uac00) \uc5f4\ub838\uc744 \ub54c", + "plugged_in": "{entity_name}\uc758 \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud614\uc744 \ub54c", + "powered": "{entity_name}\uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc744 \ub54c", + "present": "{entity_name}\uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub418\uc5c8\uc744 \ub54c", + "problem": "{entity_name}\uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "smoke": "{entity_name}\uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "sound": "{entity_name}\uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c", + "unsafe": "{entity_name}\uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uac8c \ub418\uc5c8\uc744 \ub54c", + "vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c" } }, "state": { @@ -98,6 +98,10 @@ "off": "\ubcf4\ud1b5", "on": "\ub0ae\uc74c" }, + "battery_charging": { + "off": "\ucda9\uc804 \uc911\uc774 \uc544\ub2d8", + "on": "\ucda9\uc804 \uc911" + }, "cold": { "off": "\ubcf4\ud1b5", "on": "\uc800\uc628" @@ -122,6 +126,10 @@ "off": "\ubcf4\ud1b5", "on": "\uace0\uc628" }, + "light": { + "off": "\ube5b\uc774 \uc5c6\uc2b4", + "on": "\ube5b\uc744 \uac10\uc9c0\ud568" + }, "lock": { "off": "\uc7a0\uae40", "on": "\ud574\uc81c" @@ -134,6 +142,10 @@ "off": "\uc774\uc0c1\uc5c6\uc74c", "on": "\uac10\uc9c0\ub428" }, + "moving": { + "off": "\uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", + "on": "\uc6c0\uc9c1\uc784" + }, "occupancy": { "off": "\uc774\uc0c1\uc5c6\uc74c", "on": "\uac10\uc9c0\ub428" @@ -142,6 +154,10 @@ "off": "\ub2eb\ud798", "on": "\uc5f4\ub9bc" }, + "plug": { + "off": "\ud50c\ub7ec\uadf8\uac00 \ubf51\ud798", + "on": "\ud50c\ub7ec\uadf8\uac00 \uaf3d\ud798" + }, "presence": { "off": "\uc678\ucd9c", "on": "\uc7ac\uc2e4" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json index a44e16d78e2..82cd0d3ccfe 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -25,13 +25,13 @@ "is_not_locked": "{entity_name} \u5df2\u89e3\u9501", "is_not_moist": "{entity_name} \u5e72\u71e5", "is_not_moving": "{entity_name} \u9759\u6b62", - "is_not_occupied": "{entity_name}\u6ca1\u6709\u4eba", + "is_not_occupied": "{entity_name} \u65e0\u4eba", "is_not_open": "{entity_name} \u5df2\u5173\u95ed", "is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165", "is_not_powered": "{entity_name} \u672a\u901a\u7535", "is_not_present": "{entity_name} \u4e0d\u5728\u5bb6", "is_not_unsafe": "{entity_name} \u5b89\u5168", - "is_occupied": "{entity_name}\u6709\u4eba", + "is_occupied": "{entity_name} \u6709\u4eba", "is_off": "{entity_name} \u5df2\u5173\u95ed", "is_on": "{entity_name} \u5df2\u5f00\u542f", "is_open": "{entity_name} \u5df2\u6253\u5f00", @@ -51,15 +51,42 @@ "gas": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", "hot": "{entity_name} \u53d8\u70ed", "light": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u5149\u7ebf", - "locked": "{entity_name}\u5df2\u4e0a\u9501", + "locked": "{entity_name} \u88ab\u9501\u5b9a", + "moist": "{entity_name} \u53d8\u6e7f", "motion": "{entity_name} \u68c0\u6d4b\u5230\u6709\u4eba", - "moving": "{entity_name}\u5f00\u59cb\u79fb\u52a8", - "no_motion": "{entity_name} \u672a\u68c0\u6d4b\u5230\u6709\u4eba", - "not_bat_low": "{entity_name}\u7535\u91cf\u6b63\u5e38", - "not_locked": "{entity_name}\u5df2\u89e3\u9501", - "not_opened": "{entity_name}\u5df2\u5173\u95ed", + "moving": "{entity_name} \u5f00\u59cb\u79fb\u52a8", + "no_gas": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", + "no_light": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u5149\u7ebf", + "no_motion": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u6709\u4eba", + "no_problem": "{entity_name} \u95ee\u9898\u89e3\u9664", + "no_smoke": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u70df\u96fe", + "no_sound": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u58f0\u97f3", + "no_vibration": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u632f\u52a8", + "not_bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u6b63\u5e38", + "not_cold": "{entity_name} \u4e0d\u51b7\u4e86", + "not_connected": "{entity_name} \u65ad\u5f00", + "not_hot": "{entity_name} \u4e0d\u70ed\u4e86", + "not_locked": "{entity_name} \u89e3\u9501", + "not_moist": "{entity_name} \u53d8\u5e72", + "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52a8", + "not_occupied": "{entity_name} \u4e0d\u518d\u6709\u4eba", + "not_opened": "{entity_name} \u5df2\u5173\u95ed", + "not_plugged_in": "{entity_name} \u88ab\u62d4\u51fa", + "not_powered": "{entity_name} \u6389\u7535", + "not_present": "{entity_name} \u4e0d\u5728\u5bb6", + "not_unsafe": "{entity_name} \u5b89\u5168\u4e86", + "occupied": "{entity_name} \u6709\u4eba", + "opened": "{entity_name} \u88ab\u6253\u5f00", + "plugged_in": "{entity_name} \u88ab\u63d2\u5165", + "powered": "{entity_name} \u4e0a\u7535", + "present": "{entity_name} \u5728\u5bb6", + "problem": "{entity_name} \u53d1\u73b0\u95ee\u9898", + "smoke": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u70df\u96fe", + "sound": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u58f0\u97f3", "turned_off": "{entity_name} \u88ab\u5173\u95ed", - "turned_on": "{entity_name} \u88ab\u6253\u5f00" + "turned_on": "{entity_name} \u88ab\u6253\u5f00", + "unsafe": "{entity_name} \u4e0d\u518d\u5b89\u5168", + "vibration": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u632f\u52a8" } }, "state": { @@ -120,8 +147,8 @@ "on": "\u6b63\u5728\u79fb\u52a8" }, "occupancy": { - "off": "\u672a\u89e6\u53d1", - "on": "\u5df2\u89e6\u53d1" + "off": "\u65e0\u4eba", + "on": "\u6709\u4eba" }, "opening": { "off": "\u5173\u95ed", diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index c748b2f72f9..4acce03d6fa 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -5,7 +5,7 @@ import logging from blockchain import exchangerates, statistics import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CURRENCY, @@ -14,7 +14,6 @@ from homeassistant.const import ( TIME_SECONDS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class BitcoinSensor(Entity): +class BitcoinSensor(SensorEntity): """Representation of a Bitcoin sensor.""" def __init__(self, data, option_type, currency): @@ -110,7 +109,7 @@ class BitcoinSensor(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index c1cee98208c..d0cade31a72 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -1,11 +1,12 @@ """Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service.""" +from contextlib import suppress + from bizkaibus.bizkaibus import BizkaibusData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTR_DUE_IN = "Due in" @@ -33,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([BizkaibusSensor(data, stop, route, name)], True) -class BizkaibusSensor(Entity): +class BizkaibusSensor(SensorEntity): """The class for handling the data.""" def __init__(self, data, stop, route, name): @@ -62,10 +63,8 @@ class BizkaibusSensor(Entity): def update(self): """Get the latest data from the webservice.""" self.data.update() - try: + with suppress(TypeError): self._state = self.data.info[0][ATTR_DUE_IN] - except TypeError: - pass class Bizkaibus: diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 3d3c997596a..c5f723b6858 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -22,11 +22,6 @@ PLATFORMS = ["cover", "sensor", "switch", "air_quality", "light", "climate"] PARALLEL_UPDATES = 0 -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the BleBox devices component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up BleBox devices from a config entry.""" @@ -48,9 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): domain_entry = domain.setdefault(entry.entry_id, {}) product = domain_entry.setdefault(PRODUCT, product) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 84f4c19371d..c1b9d8501c1 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,6 +1,6 @@ """BleBox sensor entities.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from . import BleBoxEntity, create_blebox_entities from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class BleBoxSensorEntity(BleBoxEntity, Entity): +class BleBoxSensorEntity(BleBoxEntity, SensorEntity): """Representation of a BleBox sensor feature.""" @property diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index 9b0bf1c0ddf..9649d70d976 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-vel rendelkezik. El\u0151sz\u00f6r friss\u00edtse." + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. El\u0151sz\u00f6r friss\u00edtsd." }, "flow_title": "BleBox eszk\u00f6z: {name} ({host})", "step": { diff --git a/homeassistant/components/blebox/translations/id.json b/homeassistant/components/blebox/translations/id.json new file mode 100644 index 00000000000..2ef604d1bff --- /dev/null +++ b/homeassistant/components/blebox/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "Perangkat BleBox sudah dikonfigurasi di {address}.", + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan", + "unsupported_version": "Firmware Perangkat BleBox sudah usang. Tingkatkan terlebih dulu." + }, + "flow_title": "Perangkat BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Alamat IP", + "port": "Port" + }, + "description": "Siapkan BleBox Anda untuk diintegrasikan dengan Home Assistant.", + "title": "Siapkan perangkat BleBox Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/ko.json b/homeassistant/components/blebox/translations/ko.json index 81c7bf3af48..1032e873ae9 100644 --- a/homeassistant/components/blebox/translations/ko.json +++ b/homeassistant/components/blebox/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "address_already_configured": "BleBox \uae30\uae30\uac00 {address} \ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "address_already_configured": "BleBox \uae30\uae30\uac00 {address}(\uc73c)\ub85c \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { @@ -16,7 +16,7 @@ "host": "IP \uc8fc\uc18c", "port": "\ud3ec\ud2b8" }, - "description": "Home Assistant \uc5d0 BleBox \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "description": "Home Assistant\uc5d0 BleBox \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", "title": "BleBox \uae30\uae30 \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 0809018522e..9c73ee6f995 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -16,6 +16,7 @@ from homeassistant.components.blink.const import ( SERVICE_SAVE_VIDEO, SERVICE_SEND_PIN, ) +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -49,7 +50,7 @@ def _reauth_flow_wrapper(hass, data): """Reauth flow wrapper.""" hass.add_job( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=data + DOMAIN, context={"source": SOURCE_REAUTH}, data=data ) ) persistent_notification.async_create( @@ -59,26 +60,25 @@ def _reauth_flow_wrapper(hass, data): ) -async def async_setup(hass, config): - """Set up a Blink component.""" - hass.data[DOMAIN] = {} - return True - - async def async_migrate_entry(hass, entry): """Handle migration of a previous version config entry.""" + _LOGGER.debug("Migrating from version %s", entry.version) data = {**entry.data} if entry.version == 1: data.pop("login_response", None) await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data) return False + if entry.version == 2: + await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data) + return False return True async def async_setup_entry(hass, entry): """Set up Blink via config entry.""" - _async_import_options_from_data_if_missing(hass, entry) + hass.data.setdefault(DOMAIN, {}) + _async_import_options_from_data_if_missing(hass, entry) hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( _blink_startup_wrapper, hass, entry ) @@ -86,9 +86,9 @@ async def async_setup_entry(hass, entry): if not hass.data[DOMAIN][entry.entry_id].available: raise ConfigEntryNotReady - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) def blink_refresh(event_time=None): @@ -133,8 +133,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -148,7 +148,7 @@ async def async_unload_entry(hass, entry): return True hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO_SCHEMA) + hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO) hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN) return True diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index dbcb6d30143..ed2b46acaa1 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -62,7 +62,7 @@ class BlinkSyncModule(AlarmControlPanelEntity): return f"{DOMAIN} {self._name}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attr = self.sync.attributes attr["network_info"] = self.data.networks diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index d4282bed606..a25b978ee7b 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -60,7 +60,7 @@ class BlinkCamera(Camera): return self._unique_id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the camera attributes.""" return self._camera.attributes diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index 5c77add3118..c6c3f0b27be 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -44,7 +44,7 @@ def _send_blink_2fa_pin(auth, pin): class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Blink config flow.""" - VERSION = 2 + VERSION = 3 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 1c91f1a2295..c88e13cdde7 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": ["blinkpy==0.17.0"], "codeowners": ["@fronzbot"], + "dhcp": [{"hostname":"blink*","macaddress":"B85F98*"}], "config_flow": true } diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 3c3adf6d990..1ec61900091 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -1,13 +1,13 @@ """Support for Blink system camera sensors.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH @@ -34,7 +34,7 @@ async def async_setup_entry(hass, config, async_add_entities): async_add_entities(entities) -class BlinkSensor(Entity): +class BlinkSensor(SensorEntity): """A Blink camera sensor.""" def __init__(self, data, camera, sensor_type): diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index dc6491e2139..6ea4e2aa9ac 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -21,8 +21,8 @@ save_video: example: "/tmp/video.mp4" send_pin: - description: Send a new pin to blink for 2FA. + description: Send a new PIN to blink for 2FA. fields: pin: - description: Pin received from blink. Leave empty if you only received a verification email. + description: PIN received from blink. Leave empty if you only received a verification email. example: "abc123" diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index db9bdf96273..6e438b58590 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -11,7 +11,7 @@ "2fa": { "title": "Two-factor authentication", "data": { "2fa": "Two-factor code" }, - "description": "Enter the pin sent to your email" + "description": "Enter the PIN sent to your email" } }, "error": { diff --git a/homeassistant/components/blink/translations/en.json b/homeassistant/components/blink/translations/en.json index 9a0a2636d3e..c8c154418df 100644 --- a/homeassistant/components/blink/translations/en.json +++ b/homeassistant/components/blink/translations/en.json @@ -14,7 +14,7 @@ "data": { "2fa": "Two-factor code" }, - "description": "Enter the pin sent to your email", + "description": "Enter the PIN sent to your email", "title": "Two-factor authentication" }, "user": { diff --git a/homeassistant/components/blink/translations/et.json b/homeassistant/components/blink/translations/et.json index 24de0ccbefd..a5cae0eaae2 100644 --- a/homeassistant/components/blink/translations/et.json +++ b/homeassistant/components/blink/translations/et.json @@ -14,7 +14,7 @@ "data": { "2fa": "2FA kood" }, - "description": "Sisesta E-posti aadressile saadetud PIN-kood", + "description": "Sisesta e-posti aadressile saadetud PIN kood", "title": "Kaheastmeline tuvastamine (2FA)" }, "user": { diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json index 83aaad902a1..23bb7fb91dd 100644 --- a/homeassistant/components/blink/translations/fr.json +++ b/homeassistant/components/blink/translations/fr.json @@ -14,7 +14,7 @@ "data": { "2fa": "Code \u00e0 deux facteurs" }, - "description": "Entrez le code PIN envoy\u00e9 \u00e0 votre e-mail", + "description": "Entrez le NIP envoy\u00e9 \u00e0 votre e-mail", "title": "Authentification \u00e0 deux facteurs" }, "user": { diff --git a/homeassistant/components/blink/translations/hu.json b/homeassistant/components/blink/translations/hu.json index 1150cda9ea9..e56b142a5b0 100644 --- a/homeassistant/components/blink/translations/hu.json +++ b/homeassistant/components/blink/translations/hu.json @@ -4,10 +4,19 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "2fa": { + "data": { + "2fa": "K\u00e9tfaktoros k\u00f3d" + }, + "description": "Add meg az e-mail c\u00edmedre k\u00fcld\u00f6tt pint", + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/blink/translations/id.json b/homeassistant/components/blink/translations/id.json new file mode 100644 index 00000000000..bdbc406bda7 --- /dev/null +++ b/homeassistant/components/blink/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_access_token": "Token akses tidak valid", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kode autentikasi dua faktor" + }, + "description": "Masukkan PIN yang dikirimkan ke email Anda", + "title": "Autentikasi dua faktor" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Masuk dengan akun Blink" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Interval Pindai (detik)" + }, + "description": "Konfigurasikan integrasi Blink", + "title": "Opsi Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/it.json b/homeassistant/components/blink/translations/it.json index bdb0ba3f6b4..6dee5d9c02f 100644 --- a/homeassistant/components/blink/translations/it.json +++ b/homeassistant/components/blink/translations/it.json @@ -14,7 +14,7 @@ "data": { "2fa": "Codice a due fattori" }, - "description": "Inserisci il pin inviato alla tua email", + "description": "Inserisci il PIN inviato alla tua email", "title": "Autenticazione a due fattori" }, "user": { diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json index 35ef0cefdef..6d42cdbb2e9 100644 --- a/homeassistant/components/blink/translations/ko.json +++ b/homeassistant/components/blink/translations/ko.json @@ -14,7 +14,7 @@ "data": { "2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc" }, - "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "2\ub2e8\uacc4 \uc778\uc99d" }, "user": { diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json index f1f1ce7888b..3160ffe8ddd 100644 --- a/homeassistant/components/blink/translations/nl.json +++ b/homeassistant/components/blink/translations/nl.json @@ -29,6 +29,10 @@ "options": { "step": { "simple_options": { + "data": { + "scan_interval": "Scaninterval (seconden)" + }, + "description": "Configureer Blink-integratie", "title": "Blink opties" } } diff --git a/homeassistant/components/blink/translations/no.json b/homeassistant/components/blink/translations/no.json index 0b99005c382..90f8fcaa06b 100644 --- a/homeassistant/components/blink/translations/no.json +++ b/homeassistant/components/blink/translations/no.json @@ -14,7 +14,7 @@ "data": { "2fa": "Totrinnsbekreftelse kode" }, - "description": "Skriv inn pin-koden som ble sendt til din e-posten", + "description": "Skriv inn PIN-koden som er sendt til e-posten din", "title": "Totrinnsbekreftelse" }, "user": { diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json index 13fe2f1fc93..72b6c32e5be 100644 --- a/homeassistant/components/blink/translations/pl.json +++ b/homeassistant/components/blink/translations/pl.json @@ -14,7 +14,7 @@ "data": { "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" }, - "description": "Wpisz kod PIN wys\u0142any na Tw\u00f3j adres e-mail. Je\u015bli.", + "description": "Wprowad\u017a kod PIN wys\u0142any na Tw\u00f3j adres e-mail.", "title": "Uwierzytelnianie dwusk\u0142adnikowe" }, "user": { diff --git a/homeassistant/components/blink/translations/ru.json b/homeassistant/components/blink/translations/ru.json index 0835ab5ac0a..fa68ee2dad4 100644 --- a/homeassistant/components/blink/translations/ru.json +++ b/homeassistant/components/blink/translations/ru.json @@ -20,7 +20,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Blink" } diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index d2c42bf5531..6874efb6e31 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -12,10 +12,10 @@ "step": { "2fa": { "data": { - "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc" + "2fa": "\u96d9\u91cd\u8a8d\u8b49\u78bc" }, "description": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 PIN \u78bc", - "title": "\u96d9\u91cd\u9a57\u8b49" + "title": "\u96d9\u91cd\u8a8d\u8b49" }, "user": { "data": { diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index 0d1aff7b826..bb9bbf315e4 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -26,7 +26,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Blinkt Light platform.""" - # pylint: disable=no-member blinkt = importlib.import_module("blinkt") # ensure that the lights are off when exiting diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index feb9d582cff..3ecf4bee319 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -5,10 +5,9 @@ import logging from pyblockchain import get_balance, validate_address import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([BlockchainSensor(name, addresses)], True) -class BlockchainSensor(Entity): +class BlockchainSensor(SensorEntity): """Representation of a Blockchain.com sensor.""" def __init__(self, name, addresses): @@ -75,7 +74,7 @@ class BlockchainSensor(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index cd993e0332a..fa8d3160dc8 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -18,7 +18,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -BLOOMSKY_TYPE = ["camera", "binary_sensor", "sensor"] +PLATFORMS = ["camera", "binary_sensor", "sensor"] DOMAIN = "bloomsky" @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): - """Set up the BloomSky component.""" + """Set up the BloomSky integration.""" api_key = config[DOMAIN][CONF_API_KEY] try: @@ -42,8 +42,8 @@ def setup(hass, config): hass.data[DOMAIN] = bloomsky - for component in BLOOMSKY_TYPE: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) return True @@ -60,7 +60,7 @@ class BloomSky: self._endpoint_argument = "unit=intl" if is_metric else "" self.devices = {} self.is_metric = is_metric - _LOGGER.debug("Initial BloomSky device load...") + _LOGGER.debug("Initial BloomSky device load") self.refresh_devices() @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index df06e39db7e..4dc52e1a85c 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -1,7 +1,7 @@ """Support the sensor of a BloomSky weather station.""" import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( AREA_SQUARE_METERS, CONF_MONITORED_CONDITIONS, @@ -12,7 +12,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DOMAIN @@ -70,7 +69,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([BloomSkySensor(bloomsky, device, variable)], True) -class BloomSkySensor(Entity): +class BloomSkySensor(SensorEntity): """Representation of a single sensor in a BloomSky device.""" def __init__(self, bs, device, sensor_name): diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 9e8b1260eff..309365710ad 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -1,7 +1,7 @@ """The blueprint integration.""" from . import websocket_api -from .const import DOMAIN # noqa -from .errors import ( # noqa +from .const import DOMAIN # noqa: F401 +from .errors import ( # noqa: F401 BlueprintException, BlueprintWithNameException, FailedToLoad, @@ -9,8 +9,8 @@ from .errors import ( # noqa InvalidBlueprintInputs, MissingInput, ) -from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa -from .schemas import is_blueprint_instance_config # noqa +from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa: F401 +from .schemas import is_blueprint_instance_config # noqa: F401 async def async_setup(hass, config): diff --git a/homeassistant/components/blueprint/const.py b/homeassistant/components/blueprint/const.py index 60df20dda36..a91d30199c9 100644 --- a/homeassistant/components/blueprint/const.py +++ b/homeassistant/components/blueprint/const.py @@ -5,7 +5,6 @@ CONF_BLUEPRINT = "blueprint" CONF_USE_BLUEPRINT = "use_blueprint" CONF_INPUT = "input" CONF_SOURCE_URL = "source_url" -CONF_DESCRIPTION = "description" CONF_HOMEASSISTANT = "homeassistant" CONF_MIN_VERSION = "min_version" diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index 217851df980..99dffb114e1 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -1,8 +1,9 @@ """Import logic for blueprint.""" +from __future__ import annotations + from dataclasses import dataclass import html import re -from typing import Optional import voluptuous as vol import yarl @@ -93,7 +94,7 @@ def _get_community_post_import_url(url: str) -> str: def _extract_blueprint_from_community_topic( url: str, topic: dict, -) -> Optional[ImportedBlueprint]: +) -> ImportedBlueprint | None: """Extract a blueprint from a community post JSON. Async friendly. @@ -136,7 +137,7 @@ def _extract_blueprint_from_community_topic( async def fetch_blueprint_from_community_post( hass: HomeAssistant, url: str -) -> Optional[ImportedBlueprint]: +) -> ImportedBlueprint | None: """Get blueprints from a community post url. Method can raise aiohttp client exceptions, vol.Invalid. diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 84931a04310..797f9bd1512 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -1,9 +1,11 @@ """Blueprint models.""" +from __future__ import annotations + import asyncio import logging import pathlib import shutil -from typing import Any, Dict, List, Optional, Union +from typing import Any from awesomeversion import AwesomeVersion import voluptuous as vol @@ -49,8 +51,8 @@ class Blueprint: self, data: dict, *, - path: Optional[str] = None, - expected_domain: Optional[str] = None, + path: str | None = None, + expected_domain: str | None = None, ) -> None: """Initialize a blueprint.""" try: @@ -95,7 +97,7 @@ class Blueprint: """Return blueprint metadata.""" return self.data[CONF_BLUEPRINT] - def update_metadata(self, *, source_url: Optional[str] = None) -> None: + def update_metadata(self, *, source_url: str | None = None) -> None: """Update metadata.""" if source_url is not None: self.data[CONF_BLUEPRINT][CONF_SOURCE_URL] = source_url @@ -105,7 +107,7 @@ class Blueprint: return yaml.dump(self.data) @callback - def validate(self) -> Optional[List[str]]: + def validate(self) -> list[str] | None: """Test if the Home Assistant installation supports this blueprint. Return list of errors if not valid. @@ -126,7 +128,7 @@ class BlueprintInputs: """Inputs for a blueprint.""" def __init__( - self, blueprint: Blueprint, config_with_inputs: Dict[str, Any] + self, blueprint: Blueprint, config_with_inputs: dict[str, Any] ) -> None: """Instantiate a blueprint inputs object.""" self.blueprint = blueprint @@ -218,7 +220,7 @@ class DomainBlueprints: blueprint_data, expected_domain=self.domain, path=blueprint_path ) - def _load_blueprints(self) -> Dict[str, Union[Blueprint, BlueprintException]]: + def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException]: """Load all the blueprints.""" blueprint_folder = pathlib.Path( self.hass.config.path(BLUEPRINT_FOLDER, self.domain) @@ -243,7 +245,7 @@ class DomainBlueprints: async def async_get_blueprints( self, - ) -> Dict[str, Union[Blueprint, BlueprintException]]: + ) -> dict[str, Blueprint | BlueprintException]: """Get all the blueprints.""" async with self._load_lock: return await self.hass.async_add_executor_job(self._load_blueprints) diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index 07d8e8b0128..f16598afac2 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_DEFAULT, + CONF_DESCRIPTION, CONF_DOMAIN, CONF_NAME, CONF_PATH, @@ -15,7 +16,6 @@ from homeassistant.helpers import config_validation as cv, selector from .const import ( CONF_BLUEPRINT, - CONF_DESCRIPTION, CONF_HOMEASSISTANT, CONF_INPUT, CONF_MIN_VERSION, diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 05ae2816696..b8a4c214a2e 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -1,5 +1,5 @@ """Websocket API for blueprint.""" -from typing import Dict, Optional +from __future__ import annotations import async_timeout import voluptuous as vol @@ -33,7 +33,7 @@ def async_setup(hass: HomeAssistant): ) async def ws_list_blueprints(hass, connection, msg): """List available blueprints.""" - domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get( + domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get( DOMAIN, {} ) results = {} @@ -102,7 +102,7 @@ async def ws_save_blueprint(hass, connection, msg): path = msg["path"] domain = msg["domain"] - domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get( + domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get( DOMAIN, {} ) @@ -149,7 +149,7 @@ async def ws_delete_blueprint(hass, connection, msg): path = msg["path"] domain = msg["domain"] - domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get( + domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get( DOMAIN, {} ) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 0ace6f5679a..dff45ca68bd 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -850,7 +850,7 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.error("Master not found %s", master_device) @property - def device_state_attributes(self): + def extra_state_attributes(self): """List members in group.""" attributes = {} if self._group_list: diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 76df4e65ac7..9ac79afde2c 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta import logging from uuid import UUID -import pygatt # pylint: disable=import-error +import pygatt import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 380d8091bd6..f00bd672892 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,10 +1,10 @@ """Tracking for bluetooth devices.""" +from __future__ import annotations + import asyncio import logging -from typing import List, Optional, Set, Tuple -# pylint: disable=import-error -import bluetooth +import bluetooth # pylint: disable=import-error from bt_proximity import BluetoothRSSI import voluptuous as vol @@ -51,7 +51,7 @@ def is_bluetooth_device(device) -> bool: return device.mac and device.mac[:3].upper() == BT_PREFIX -def discover_devices(device_id: int) -> List[Tuple[str, str]]: +def discover_devices(device_id: int) -> list[tuple[str, str]]: """Discover Bluetooth devices.""" result = bluetooth.discover_devices( duration=8, @@ -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: HomeAssistantType) -> tuple[set[str], set[str]]: """ Load all known devices. @@ -91,17 +91,17 @@ async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[s devices = await async_load_config(yaml_path, hass, 0) bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] - devices_to_track: Set[str] = { + devices_to_track: set[str] = { device.mac[3:] for device in bluetooth_devices if device.track } - devices_to_not_track: Set[str] = { + devices_to_not_track: set[str] = { device.mac[3:] for device in bluetooth_devices if not device.track } return devices_to_track, devices_to_not_track -def lookup_name(mac: str) -> Optional[str]: +def lookup_name(mac: str) -> str | None: """Lookup a Bluetooth device name.""" _LOGGER.debug("Scanning %s", mac) return bluetooth.lookup_name(mac, timeout=5) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 265ec01b6db..2c3ab0303b0 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -1,13 +1,14 @@ """Support for BME280 temperature, humidity and pressure sensor.""" +from contextlib import suppress from datetime import timedelta from functools import partial import logging from i2csense.bme280 import BME280 # pylint: disable=import-error -import smbus # pylint: disable=import-error +import smbus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, @@ -15,7 +16,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -111,13 +111,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensor_handler = await hass.async_add_executor_job(BME280Handler, sensor) dev = [] - try: + with suppress(KeyError): for variable in config[CONF_MONITORED_CONDITIONS]: dev.append( BME280Sensor(sensor_handler, variable, SENSOR_TYPES[variable][1], name) ) - except KeyError: - pass async_add_entities(dev, True) @@ -136,7 +134,7 @@ class BME280Handler: self.sensor.update(first_reading) -class BME280Sensor(Entity): +class BME280Sensor(SensorEntity): """Implementation of the BME280 sensor.""" def __init__(self, bme280_client, sensor_type, temp_unit, name): diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 49d95bbc53b..f3d6b9428ea 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -4,10 +4,10 @@ import threading from time import monotonic, sleep import bme680 # pylint: disable=import-error -from smbus import SMBus # pylint: disable=import-error +from smbus import SMBus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, @@ -15,7 +15,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) @@ -131,7 +130,6 @@ def _setup_bme680(config): sensor_handler = None sensor = None try: - # pylint: disable=no-member i2c_address = config[CONF_I2C_ADDRESS] bus = SMBus(config[CONF_I2C_BUS]) sensor = bme680.BME680(i2c_address, bus) @@ -317,7 +315,7 @@ class BME680Handler: return hum_score + gas_score -class BME680Sensor(Entity): +class BME680Sensor(SensorEntity): """Implementation of the BME680 sensor.""" def __init__(self, bme680_client, sensor_type, temp_unit, name): diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json index dbd79896718..e22c275ed76 100644 --- a/homeassistant/components/bmp280/manifest.json +++ b/homeassistant/components/bmp280/manifest.json @@ -3,6 +3,6 @@ "name": "Bosch BMP280 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bmp280", "codeowners": ["@belidzs"], - "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.0"], + "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.1a4"], "quality_scale": "silver" } diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 3c34408cb62..60cbdb75d41 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -11,11 +11,11 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, + SensorEntity, ) from homeassistant.const import CONF_NAME, PRESSURE_HPA, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class Bmp280Sensor(Entity): +class Bmp280Sensor(SensorEntity): """Base class for BMP280 entities.""" def __init__( diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index a8bebfbc617..ebf1fd6f74e 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -59,7 +59,7 @@ DEFAULT_OPTIONS = { CONF_USE_LOCATION: False, } -BMW_PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] +PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] UPDATE_INTERVAL = 5 # in minutes SERVICE_UPDATE_STATE = "update_state" @@ -138,13 +138,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await _async_update_all() - for platform in BMW_PLATFORMS: + for platform in PLATFORMS: if platform != NOTIFY_DOMAIN: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) - # set up notify platform, no entry support for notify component yet, + # set up notify platform, no entry support for notify platform yet, # have to use discovery to load platform. hass.async_create_task( discovery.async_load_platform( @@ -164,9 +164,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in BMW_PLATFORMS - if component != NOTIFY_DOMAIN + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + if platform != NOTIFY_DOMAIN ] ) ) @@ -334,7 +334,7 @@ class BMWConnectedDriveBaseEntity(Entity): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return self._attrs diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index cad5426d548..bebb55bbde0 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -109,7 +109,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state result = self._attrs.copy() diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index fbfa20aff1a..7798dc4e7e8 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME from homeassistant.core import callback -from . import DOMAIN # pylint: disable=unused-import +from . import DOMAIN from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_USE_LOCATION DATA_SCHEMA = vol.Schema( diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 0d281e78f14..97c9be7216b 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -52,7 +52,7 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the lock.""" vehicle_state = self._vehicle.state result = self._attrs.copy() diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 480aac34eb3..38415d0006f 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -3,6 +3,7 @@ import logging from bimmer_connected.state import ChargingState +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -12,7 +13,6 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity @@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, Entity): +class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" def __init__(self, account, vehicle, attribute: str, attribute_info): diff --git a/homeassistant/components/bmw_connected_drive/translations/bg.json b/homeassistant/components/bmw_connected_drive/translations/bg.json new file mode 100644 index 00000000000..67a484573aa --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json index 12a870b4cc9..d274719d7d0 100644 --- a/homeassistant/components/bmw_connected_drive/translations/de.json +++ b/homeassistant/components/bmw_connected_drive/translations/de.json @@ -11,6 +11,7 @@ "user": { "data": { "password": "Passwort", + "region": "ConnectedDrive Region", "username": "Benutzername" } } diff --git a/homeassistant/components/bmw_connected_drive/translations/hu.json b/homeassistant/components/bmw_connected_drive/translations/hu.json new file mode 100644 index 00000000000..8724f525626 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "region": "ConnectedDrive R\u00e9gi\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/id.json b/homeassistant/components/bmw_connected_drive/translations/id.json new file mode 100644 index 00000000000..e49e9202dbe --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "region": "Wilayah ConnectedDrive", + "username": "Nama Pengguna" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Hanya baca (hanya sensor dan notifikasi, tidak ada eksekusi layanan, tidak ada fitur penguncian)", + "use_location": "Gunakan lokasi Asisten Rumah untuk polling lokasi mobil (diperlukan untuk kendaraan non i3/i8 yang diproduksi sebelum Juli 2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/ko.json b/homeassistant/components/bmw_connected_drive/translations/ko.json index 9cc079cf1cd..4c9872573be 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ko.json +++ b/homeassistant/components/bmw_connected_drive/translations/ko.json @@ -11,9 +11,20 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", + "region": "ConnectedDrive \uc9c0\uc5ed", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\uc77d\uae30 \uc804\uc6a9(\uc13c\uc11c \ubc0f \uc54c\ub9bc\ub9cc \uac00\ub2a5, \uc11c\ube44\uc2a4 \uc2e4\ud589 \ubc0f \uc7a0\uae08 \uc5c6\uc74c)", + "use_location": "\ucc28\ub7c9 \uc704\uce58 \ud3f4\ub9c1\uc5d0 Home Assistant \uc704\uce58\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4(2014\ub144 7\uc6d4 \uc774\uc804\uc5d0 \uc0dd\uc0b0\ub41c i3/i8\uc774 \uc544\ub2cc \ucc28\ub7c9\uc758 \uacbd\uc6b0 \ud544\uc694)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/nl.json b/homeassistant/components/bmw_connected_drive/translations/nl.json index 83ae0b9ff7d..8fa6c839112 100644 --- a/homeassistant/components/bmw_connected_drive/translations/nl.json +++ b/homeassistant/components/bmw_connected_drive/translations/nl.json @@ -16,5 +16,15 @@ } } } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Alleen-lezen (alleen sensoren en notificatie, geen uitvoering van diensten, geen vergrendeling)", + "use_location": "Gebruik Home Assistant locatie voor auto locatie peilingen (vereist voor niet i3/i8 voertuigen geproduceerd voor 7/2014)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/ru.json b/homeassistant/components/bmw_connected_drive/translations/ru.json index 9ac76bbea9e..8ab4e4e1207 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ru.json +++ b/homeassistant/components/bmw_connected_drive/translations/ru.json @@ -12,7 +12,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "region": "\u0420\u0435\u0433\u0438\u043e\u043d ConnectedDrive", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 9d0a613000a..0fafb61df35 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -19,13 +19,7 @@ PLATFORMS = ["cover", "fan", "light", "switch"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Bond component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Bond from a config entry.""" host = entry.data[CONF_HOST] token = entry.data[CONF_ACCESS_TOKEN] @@ -41,6 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): bpup_subs = BPUPSubscriptions() stop_bpup = await start_bpup(host, bpup_subs) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { HUB: hub, BPUP_SUBS: bpup_subs, @@ -50,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id) + assert hub.bond_id is not None hub_name = hub.name or hub.bond_id device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( @@ -64,9 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -77,8 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -96,7 +92,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_remove_old_device_identifiers( config_entry_id: str, device_registry: dr.DeviceRegistry, hub: BondHub -): +) -> None: """Remove the non-unique device registry entries.""" for device in hub.devices: dev = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)}) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index f81e3a0be5c..763a0957876 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Bond integration.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional, Tuple +from typing import Any from aiohttp import ClientConnectionError, ClientResponseError from bond_api import Bond @@ -13,8 +15,9 @@ from homeassistant.const import ( CONF_NAME, HTTP_UNAUTHORIZED, ) +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN from .utils import BondHub _LOGGER = logging.getLogger(__name__) @@ -27,7 +30,7 @@ DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) TOKEN_SCHEMA = vol.Schema({}) -async def _validate_input(data: Dict[str, Any]) -> Tuple[str, Optional[str]]: +async def _validate_input(data: dict[str, Any]) -> tuple[str, str]: """Validate the user input allows us to connect.""" bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) @@ -57,11 +60,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH - def __init__(self): + def __init__(self) -> None: """Initialize config flow.""" - self._discovered: dict = None + self._discovered: dict[str, str] = {} - async def _async_try_automatic_configure(self): + async def _async_try_automatic_configure(self) -> None: """Try to auto configure the device. Failure is acceptable here since the device may have been @@ -82,9 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _, hub_name = await _validate_input(self._discovered) self._discovered[CONF_NAME] = hub_name - async def async_step_zeroconf( - self, discovery_info: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[str, Any]: # type: ignore """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info[CONF_NAME] host: str = discovery_info[CONF_HOST] @@ -107,8 +108,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() async def async_step_confirm( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: @@ -148,8 +149,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_user( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 6b3c8d6bc02..60dcc4ec1f0 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -1,5 +1,7 @@ """Support for Bond covers.""" -from typing import Any, Callable, List, Optional +from __future__ import annotations + +from typing import Any, Callable from bond_api import Action, BPUPSubscriptions, DeviceType @@ -16,14 +18,14 @@ from .utils import BondDevice, BondHub async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Bond cover devices.""" data = hass.data[DOMAIN][entry.entry_id] hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] - covers = [ + covers: list[Entity] = [ BondCover(hub, device, bpup_subs) for device in hub.devices if device.type == DeviceType.MOTORIZED_SHADES @@ -35,23 +37,25 @@ async def async_setup_entry( class BondCover(BondEntity, CoverEntity): """Representation of a Bond cover.""" - def __init__(self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions): + def __init__( + self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions + ) -> None: """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) - self._closed: Optional[bool] = None + self._closed: bool | None = None - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: cover_open = state.get("open") self._closed = True if cover_open == 0 else False if cover_open == 1 else None @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Get device class.""" return DEVICE_CLASS_SHADE @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" return self._closed @@ -63,6 +67,6 @@ class BondCover(BondEntity, CoverEntity): """Close cover.""" await self._hub.bond.action(self._device.device_id, Action.close()) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Hold cover.""" await self._hub.bond.action(self._device.device_id, Action.hold()) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index ec885f454e3..a676d99e9ad 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -1,9 +1,11 @@ """An abstract class common to all Bond entities.""" +from __future__ import annotations + from abc import abstractmethod from asyncio import Lock, TimeoutError as AsyncIOTimeoutError from datetime import timedelta import logging -from typing import Any, Dict, Optional +from typing import Any from aiohttp import ClientError from bond_api import BPUPSubscriptions @@ -29,7 +31,7 @@ class BondEntity(Entity): hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions, - sub_device: Optional[str] = None, + sub_device: str | None = None, ): """Initialize entity with API and device info.""" self._hub = hub @@ -38,11 +40,11 @@ class BondEntity(Entity): self._sub_device = sub_device self._available = True self._bpup_subs = bpup_subs - self._update_lock = None + self._update_lock: Lock | None = None self._initialized = False @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Get unique ID for the entity.""" hub_id = self._hub.bond_id device_id = self._device_id @@ -50,7 +52,7 @@ class BondEntity(Entity): return f"{hub_id}_{device_id}{sub_device_id}" @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Get entity name.""" if self._sub_device: sub_device_name = self._sub_device.replace("_", " ").title() @@ -58,12 +60,12 @@ class BondEntity(Entity): return self._device.name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed.""" return False @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> dict[str, Any] | None: """Get a an HA device representing this Bond controlled device.""" device_info = { ATTR_NAME: self.name, @@ -96,15 +98,16 @@ class BondEntity(Entity): """Report availability of this entity based on last API call results.""" return self._available - async def async_update(self): + async def async_update(self) -> None: """Fetch assumed state of the cover from the hub using API.""" await self._async_update_from_api() - async def _async_update_if_bpup_not_alive(self, *_): + 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: return + assert self._update_lock is not None if self._update_lock.locked(): _LOGGER.warning( "Updating %s took longer than the scheduled update interval %s", @@ -117,7 +120,7 @@ class BondEntity(Entity): await self._async_update_from_api() self.async_write_ha_state() - async def _async_update_from_api(self): + async def _async_update_from_api(self) -> None: """Fetch via the API.""" try: state: dict = await self._hub.bond.device_state(self._device_id) @@ -131,11 +134,11 @@ class BondEntity(Entity): self._async_state_callback(state) @abstractmethod - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: raise NotImplementedError @callback - def _async_state_callback(self, state): + def _async_state_callback(self, state: dict) -> None: """Process a state change.""" self._initialized = True if not self._available: @@ -147,12 +150,12 @@ class BondEntity(Entity): self._apply_state(state) @callback - def _async_bpup_callback(self, state): + def _async_bpup_callback(self, state: dict) -> None: """Process a state change from BPUP.""" self._async_state_callback(state) self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to BPUP and start polling.""" await super().async_added_to_hass() self._update_lock = Lock() diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 5ff7e0c7065..817cf0f99a2 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -1,7 +1,9 @@ """Support for Bond fans.""" +from __future__ import annotations + import logging import math -from typing import Any, Callable, List, Optional, Tuple +from typing import Any, Callable from bond_api import Action, BPUPSubscriptions, DeviceType, Direction @@ -31,14 +33,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Bond fan devices.""" data = hass.data[DOMAIN][entry.entry_id] hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] - fans = [ + fans: list[Entity] = [ BondFan(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_fan(device.type) @@ -54,11 +56,11 @@ class BondFan(BondEntity, FanEntity): """Create HA entity representing Bond fan.""" super().__init__(hub, device, bpup_subs) - self._power: Optional[bool] = None - self._speed: Optional[int] = None - self._direction: Optional[int] = None + self._power: bool | None = None + self._speed: int | None = None + self._direction: int | None = None - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: self._power = state.get("power") self._speed = state.get("speed") self._direction = state.get("direction") @@ -75,12 +77,12 @@ class BondFan(BondEntity, FanEntity): return features @property - def _speed_range(self) -> Tuple[int, int]: + def _speed_range(self) -> tuple[int, int]: """Return the range of speeds.""" return (1, self._device.props.get("max_speed", 3)) @property - def percentage(self) -> Optional[str]: + def percentage(self) -> int: """Return the current speed percentage for the fan.""" if not self._speed or not self._power: return 0 @@ -92,7 +94,7 @@ class BondFan(BondEntity, FanEntity): return int_states_in_range(self._speed_range) @property - def current_direction(self) -> Optional[str]: + def current_direction(self) -> str | None: """Return fan rotation direction.""" direction = None if self._direction == Direction.FORWARD: @@ -125,10 +127,10 @@ class BondFan(BondEntity, FanEntity): async def async_turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the fan.""" _LOGGER.debug("Fan async_turn_on called with percentage %s", percentage) @@ -142,7 +144,7 @@ class BondFan(BondEntity, FanEntity): """Turn the fan off.""" await self._hub.bond.action(self._device.device_id, Action.turn_off()) - async def async_set_direction(self, direction: str): + async def async_set_direction(self, direction: str) -> None: """Set fan rotation direction.""" bond_direction = ( Direction.REVERSE if direction == DIRECTION_REVERSE else Direction.FORWARD diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 194a009a857..8faab26f785 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -1,6 +1,8 @@ """Support for Bond lights.""" +from __future__ import annotations + import logging -from typing import Any, Callable, List, Optional +from typing import Any, Callable from bond_api import Action, BPUPSubscriptions, DeviceType @@ -24,14 +26,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Bond light devices.""" data = hass.data[DOMAIN][entry.entry_id] hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] - fan_lights: List[Entity] = [ + fan_lights: list[Entity] = [ BondLight(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_fan(device.type) @@ -39,31 +41,31 @@ async def async_setup_entry( and not (device.supports_up_light() and device.supports_down_light()) ] - fan_up_lights: List[Entity] = [ + fan_up_lights: list[Entity] = [ BondUpLight(hub, device, bpup_subs, "up_light") for device in hub.devices if DeviceType.is_fan(device.type) and device.supports_up_light() ] - fan_down_lights: List[Entity] = [ + fan_down_lights: list[Entity] = [ BondDownLight(hub, device, bpup_subs, "down_light") for device in hub.devices if DeviceType.is_fan(device.type) and device.supports_down_light() ] - fireplaces: List[Entity] = [ + fireplaces: list[Entity] = [ BondFireplace(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_fireplace(device.type) ] - fp_lights: List[Entity] = [ + fp_lights: list[Entity] = [ BondLight(hub, device, bpup_subs, "light") for device in hub.devices if DeviceType.is_fireplace(device.type) and device.supports_light() ] - lights: List[Entity] = [ + lights: list[Entity] = [ BondLight(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_light(device.type) @@ -83,11 +85,11 @@ class BondBaseLight(BondEntity, LightEntity): hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions, - sub_device: Optional[str] = None, + sub_device: str | None = None, ): """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) - self._light: Optional[int] = None + self._light: int | None = None @property def is_on(self) -> bool: @@ -95,7 +97,7 @@ class BondBaseLight(BondEntity, LightEntity): return self._light == 1 @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int: """Flag supported features.""" return 0 @@ -108,25 +110,25 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions, - sub_device: Optional[str] = None, + sub_device: str | None = None, ): """Create HA entity representing Bond light.""" super().__init__(hub, device, bpup_subs, sub_device) - self._brightness: Optional[int] = None + self._brightness: int | None = None - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: self._light = state.get("light") self._brightness = state.get("brightness") @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int: """Flag supported features.""" if self._device.supports_set_brightness(): return SUPPORT_BRIGHTNESS return 0 @property - def brightness(self) -> int: + def brightness(self) -> int | None: """Return the brightness of this light between 1..255.""" brightness_value = ( round(self._brightness * 255 / 100) if self._brightness else None @@ -152,7 +154,7 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): class BondDownLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: self._light = state.get("down_light") and state.get("light") async def async_turn_on(self, **kwargs: Any) -> None: @@ -171,7 +173,7 @@ class BondDownLight(BondBaseLight, BondEntity, LightEntity): class BondUpLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: self._light = state.get("up_light") and state.get("light") async def async_turn_on(self, **kwargs: Any) -> None: @@ -194,16 +196,16 @@ class BondFireplace(BondEntity, LightEntity): """Create HA entity representing Bond fireplace.""" super().__init__(hub, device, bpup_subs) - self._power: Optional[bool] = None + self._power: bool | None = None # Bond flame level, 0-100 - self._flame: Optional[int] = None + self._flame: int | None = None - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: self._power = state.get("power") self._flame = state.get("flame") @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int: """Flag brightness as supported feature to represent flame level.""" return SUPPORT_BRIGHTNESS @@ -230,11 +232,11 @@ class BondFireplace(BondEntity, LightEntity): await self._hub.bond.action(self._device.device_id, Action.turn_off()) @property - def brightness(self): + def brightness(self) -> int | None: """Return the flame of this fireplace converted to HA brightness between 0..255.""" return round(self._flame * 255 / 100) if self._flame else None @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Show fireplace icon for the entity.""" return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off" diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index 8319d31c714..23e99d6af30 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -1,5 +1,7 @@ """Support for Bond generic devices.""" -from typing import Any, Callable, List, Optional +from __future__ import annotations + +from typing import Any, Callable from bond_api import Action, BPUPSubscriptions, DeviceType @@ -16,14 +18,14 @@ from .utils import BondDevice, BondHub async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Bond generic devices.""" data = hass.data[DOMAIN][entry.entry_id] hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] - switches = [ + switches: list[Entity] = [ BondSwitch(hub, device, bpup_subs) for device in hub.devices if DeviceType.is_generic(device.type) @@ -39,9 +41,9 @@ class BondSwitch(BondEntity, SwitchEntity): """Create HA entity representing Bond generic device (switch).""" super().__init__(hub, device, bpup_subs) - self._power: Optional[bool] = None + self._power: bool | None = None - def _apply_state(self, state: dict): + def _apply_state(self, state: dict) -> None: self._power = state.get("power") @property diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 14f86a30bb2..1c1c7375a28 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -9,6 +9,11 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "confirm": { + "data": { + "access_token": "Zugangstoken" + } + }, "user": { "data": { "access_token": "Zugriffstoken", diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index 3b2d79a34a7..868ef455f5d 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -2,6 +2,27 @@ "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", + "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtsd, miel\u0151tt folytatn\u00e1d", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Bond: {name} ({host})", + "step": { + "confirm": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" + }, + "description": "Szeretn\u00e9d be\u00e1ll\u00edtani a(z) {name}-t?" + }, + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "host": "Hoszt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bond/translations/id.json b/homeassistant/components/bond/translations/id.json new file mode 100644 index 00000000000..56c633cf31c --- /dev/null +++ b/homeassistant/components/bond/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "old_firmware": "Firmware lama yang tidak didukung pada perangkat Bond - tingkatkan versi sebelum melanjutkan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Bond: {name} ({host})", + "step": { + "confirm": { + "data": { + "access_token": "Token Akses" + }, + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "access_token": "Token Akses", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ko.json b/homeassistant/components/bond/translations/ko.json index b44db53f7c8..33a56559f79 100644 --- a/homeassistant/components/bond/translations/ko.json +++ b/homeassistant/components/bond/translations/ko.json @@ -6,15 +6,16 @@ "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "old_firmware": "Bond \uae30\uae30\uc5d0\uc11c \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc624\ub798\ub41c \ud38c\uc6e8\uc5b4\uc785\ub2c8\ub2e4. \uacc4\uc18d\ud558\uae30 \uc804\uc5d0 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc8fc\uc138\uc694.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "\ubcf8\ub4dc : {bond_id} ( {host} )", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" }, - "description": "{bond_id} \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/nl.json b/homeassistant/components/bond/translations/nl.json index a76c7a69d7f..67812678082 100644 --- a/homeassistant/components/bond/translations/nl.json +++ b/homeassistant/components/bond/translations/nl.json @@ -6,14 +6,16 @@ "error": { "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie", + "old_firmware": "Niet-ondersteunde oude firmware op het Bond-apparaat - voer een upgrade uit voordat u doorgaat", "unknown": "Onverwachte fout" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "Bond: {name} ({host})", "step": { "confirm": { "data": { "access_token": "Toegangstoken" - } + }, + "description": "Wilt u {name} instellen?" }, "user": { "data": { diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 225eec87d98..916da69a06c 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -1,26 +1,33 @@ """Reusable utilities for the Bond component.""" -import asyncio +from __future__ import annotations + import logging -from typing import List, Optional, Set +from typing import Any, cast from aiohttp import ClientResponseError from bond_api import Action, Bond +from homeassistant.util.async_ import gather_with_concurrency + from .const import BRIDGE_MAKE +MAX_REQUESTS = 6 + _LOGGER = logging.getLogger(__name__) class BondDevice: """Helper device class to hold ID and attributes together.""" - def __init__(self, device_id: str, attrs: dict, props: dict): + def __init__( + self, device_id: str, attrs: dict[str, Any], props: dict[str, Any] + ) -> None: """Create a helper device from ID and attributes returned by API.""" self.device_id = device_id self.props = props self._attrs = attrs - def __repr__(self): + def __repr__(self) -> str: """Return readable representation of a bond device.""" return { "device_id": self.device_id, @@ -31,25 +38,25 @@ class BondDevice: @property def name(self) -> str: """Get the name of this device.""" - return self._attrs["name"] + return cast(str, self._attrs["name"]) @property def type(self) -> str: """Get the type of this device.""" - return self._attrs["type"] + return cast(str, self._attrs["type"]) @property - def location(self) -> str: + def location(self) -> str | None: """Get the location of this device.""" return self._attrs.get("location") @property - def template(self) -> str: + def template(self) -> str | None: """Return this model template.""" return self._attrs.get("template") @property - def branding_profile(self) -> str: + def branding_profile(self) -> str | None: """Return this branding profile.""" return self.props.get("branding_profile") @@ -58,9 +65,9 @@ class BondDevice: """Check if Trust State is turned on.""" return self.props.get("trust_state", False) - def _has_any_action(self, actions: Set[str]): + def _has_any_action(self, actions: set[str]) -> bool: """Check to see if the device supports any of the actions.""" - supported_actions: List[str] = self._attrs["actions"] + supported_actions: list[str] = self._attrs["actions"] for action in supported_actions: if action in actions: return True @@ -99,26 +106,36 @@ class BondHub: def __init__(self, bond: Bond): """Initialize Bond Hub.""" self.bond: Bond = bond - self._bridge: Optional[dict] = None - self._version: Optional[dict] = None - self._devices: Optional[List[BondDevice]] = None + self._bridge: dict[str, Any] = {} + self._version: dict[str, Any] = {} + self._devices: list[BondDevice] = [] - async def setup(self, max_devices=None): + async def setup(self, max_devices: int | None = None) -> None: """Read hub version information.""" self._version = await self.bond.version() _LOGGER.debug("Bond reported the following version info: %s", self._version) # Fetch all available devices using Bond API. device_ids = await self.bond.devices() self._devices = [] + setup_device_ids = [] + tasks = [] for idx, device_id in enumerate(device_ids): if max_devices is not None and idx >= max_devices: break - - device, props = await asyncio.gather( - self.bond.device(device_id), self.bond.device_properties(device_id) + setup_device_ids.append(device_id) + tasks.extend( + [self.bond.device(device_id), self.bond.device_properties(device_id)] ) - self._devices.append(BondDevice(device_id, device, props)) + responses = await gather_with_concurrency(MAX_REQUESTS, *tasks) + response_idx = 0 + for device_id in setup_device_ids: + self._devices.append( + BondDevice( + device_id, responses[response_idx], responses[response_idx + 1] + ) + ) + response_idx += 2 _LOGGER.debug("Discovered Bond devices: %s", self._devices) try: @@ -129,18 +146,18 @@ class BondHub: _LOGGER.debug("Bond reported the following bridge info: %s", self._bridge) @property - def bond_id(self) -> Optional[str]: + def bond_id(self) -> str | None: """Return unique Bond ID for this hub.""" # Old firmwares are missing the bondid return self._version.get("bondid") @property - def target(self) -> str: + def target(self) -> str | None: """Return this hub target.""" return self._version.get("target") @property - def model(self) -> str: + def model(self) -> str | None: """Return this hub model.""" return self._version.get("model") @@ -154,22 +171,22 @@ class BondHub: """Get the name of this bridge.""" if not self.is_bridge and self._devices: return self._devices[0].name - return self._bridge["name"] + return cast(str, self._bridge["name"]) @property - def location(self) -> Optional[str]: + def location(self) -> str | None: """Get the location of this bridge.""" if not self.is_bridge and self._devices: return self._devices[0].location return self._bridge.get("location") @property - def fw_ver(self) -> str: + def fw_ver(self) -> str | None: """Return this hub firmware version.""" return self._version.get("fw_ver") @property - def devices(self) -> List[BondDevice]: + def devices(self) -> list[BondDevice]: """Return a list of all devices controlled by this hub.""" return self._devices diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 46fd8675358..d8f6d64b15f 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -10,11 +10,6 @@ from .const import BRAVIARC, DOMAIN, UNDO_UPDATE_LISTENER PLATFORMS = ["media_player"] -async def async_setup(hass, config): - """Set up the Bravia TV component.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up a config entry.""" host = config_entry.data[CONF_HOST] @@ -28,9 +23,9 @@ async def async_setup_entry(hass, config_entry): UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -41,8 +36,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index d8831dd1494..1ac31972f33 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import ( # pylint:disable=unused-import +from .const import ( ATTR_CID, ATTR_MAC, ATTR_MODEL, diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index a87786df1e8..fbb23fdee04 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -1,15 +1,36 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_ip_control": "Az IP-vez\u00e9rl\u00e9s le van tiltva a TV-n, vagy a TV nem t\u00e1mogatja." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "unsupported_model": "A TV modell nem t\u00e1mogatott." + }, "step": { "authorize": { "data": { - "pin": "PIN k\u00f3d" - } + "pin": "PIN-k\u00f3d" + }, + "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "user": { "data": { "host": "Hoszt" - } + }, + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Figyelmen k\u00edv\u00fcl hagyott forr\u00e1sok list\u00e1ja" + }, + "title": "A Sony Bravia TV be\u00e1ll\u00edt\u00e1sai" } } } diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json new file mode 100644 index 00000000000..def84dacdbb --- /dev/null +++ b/homeassistant/components/braviatv/translations/id.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "unsupported_model": "Model TV Anda tidak didukung." + }, + "step": { + "authorize": { + "data": { + "pin": "Kode PIN" + }, + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.", + "title": "Otorisasi TV Sony Bravia" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Siapkan integrasi TV Sony Bravia. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/braviatv \n\nPastikan TV Anda dinyalakan.", + "title": "TV Sony Bravia" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Daftar sumber yang diabaikan" + }, + "title": "Pilihan untuk TV Sony Bravia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 0bfb6b3f1b2..7bad0f0047c 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "no_ip_control": "TV \uc5d0\uc11c IP \uc81c\uc5b4\uac00 \ube44\ud65c\uc131\ud654\ub418\uc5c8\uac70\ub098 TV \uac00 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + "no_ip_control": "TV\uc5d0\uc11c IP \uc81c\uc5b4\uac00 \ube44\ud65c\uc131\ud654\ub418\uc5c8\uac70\ub098 TV\uac00 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \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", "unsupported_model": "\uc774 TV \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "step": { @@ -14,14 +14,14 @@ "data": { "pin": "PIN \ucf54\ub4dc" }, - "description": "Sony Bravia TV \uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV \uc5d0\uc11c Home Assistant \ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device \ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.", + "description": "Sony Bravia TV\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV\uc5d0\uc11c Home Assistant\ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device\ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.", "title": "Sony Bravia TV \uc2b9\uc778\ud558\uae30" }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV \uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index b35d7de45cf..5354f5761ec 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Deze tv is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund." }, "error": { - "cannot_connect": "Geen verbinding, ongeldige host of PIN-code.", - "invalid_host": "Ongeldige hostnaam of IP-adres.", + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres", "unsupported_model": "Uw tv-model wordt niet ondersteund." }, "step": { @@ -19,7 +19,7 @@ }, "user": { "data": { - "host": "Hostnaam of IP-adres van tv" + "host": "Host" }, "description": "Stel Sony Bravia TV-integratie in. Als je problemen hebt met de configuratie ga dan naar: https://www.home-assistant.io/integrations/braviatv \n\nZorg ervoor dat uw tv is ingeschakeld.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index a309e4eb603..158f3a27113 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -13,15 +13,12 @@ from broadlink.exceptions import ( import voluptuous as vol from homeassistant import config_entries, data_entry_flow +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.helpers import config_validation as cv -from .const import ( # pylint: disable=unused-import - DEFAULT_PORT, - DEFAULT_TIMEOUT, - DOMAIN, - DOMAINS_AND_TYPES, -) +from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, DOMAINS_AND_TYPES from .helpers import format_mac _LOGGER = logging.getLogger(__name__) @@ -39,11 +36,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_set_device(self, device, raise_on_progress=True): """Define a device for the config flow.""" - supported_types = { - device_type - for device_types in DOMAINS_AND_TYPES - for device_type in device_types[1] - } + supported_types = set.union(*DOMAINS_AND_TYPES.values()) if device.type not in supported_types: _LOGGER.error( "Unsupported device: %s. If it worked before, please open " @@ -63,6 +56,28 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": device.host[0], } + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + host = dhcp_discovery[IP_ADDRESS] + unique_id = dhcp_discovery[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: + 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") + + await self.async_set_device(device) + return await self.async_step_auth() + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" errors = {} @@ -93,7 +108,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: device.timeout = timeout - if self.source != "reauth": + if self.source != SOURCE_REAUTH: await self.async_set_device(device) self._abort_if_unique_id_configured( updates={CONF_HOST: device.host[0], CONF_TIMEOUT: timeout} diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index b10f7e74ba7..fd060d23b35 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -5,11 +5,26 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN DOMAIN = "broadlink" -DOMAINS_AND_TYPES = ( - (REMOTE_DOMAIN, ("RM2", "RM4")), - (SENSOR_DOMAIN, ("A1", "RM2", "RM4")), - (SWITCH_DOMAIN, ("BG1", "MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")), -) +DOMAINS_AND_TYPES = { + REMOTE_DOMAIN: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}, + SENSOR_DOMAIN: {"A1", "RM4MINI", "RM4PRO", "RMPRO"}, + SWITCH_DOMAIN: { + "BG1", + "MP1", + "RM4MINI", + "RM4PRO", + "RMMINI", + "RMMINIB", + "RMPRO", + "SP1", + "SP2", + "SP2S", + "SP3", + "SP3S", + "SP4", + "SP4B", + }, +} DEFAULT_PORT = 80 DEFAULT_TIMEOUT = 5 diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index be9c7626ac1..5b42205993c 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -1,5 +1,6 @@ """Support for Broadlink devices.""" import asyncio +from contextlib import suppress from functools import partial import logging @@ -12,6 +13,7 @@ from broadlink.exceptions import ( NetworkTimeoutError, ) +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -22,9 +24,9 @@ from .updater import get_update_manager _LOGGER = logging.getLogger(__name__) -def get_domains(device_type): +def get_domains(dev_type): """Return the domains available for a device type.""" - return {domain for domain, types in DOMAINS_AND_TYPES if device_type in types} + return {d for d, t in DOMAINS_AND_TYPES.items() if dev_type in t} class BroadlinkDevice: @@ -94,18 +96,14 @@ class BroadlinkDevice: update_manager = get_update_manager(self) coordinator = update_manager.coordinator - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady() + await coordinator.async_config_entry_first_refresh() self.update_manager = update_manager self.hass.data[DOMAIN].devices[config.entry_id] = self self.reset_jobs.append(config.add_update_listener(self.async_update)) - try: + with suppress(BroadlinkException, OSError): self.fw_version = await self.hass.async_add_executor_job(api.get_fwversion) - except (BroadlinkException, OSError): - pass # Forward entry setup to related domains. tasks = ( @@ -174,7 +172,7 @@ class BroadlinkDevice: self.hass.async_create_task( self.hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data={CONF_NAME: self.name, **self.config.data}, ) ) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 0562bc306a5..a1437521cb6 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,13 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.16.0"], + "requirements": ["broadlink==0.17.0"], "codeowners": ["@danielhiversen", "@felipediel"], - "config_flow": true + "config_flow": true, + "dhcp": [ + {"macaddress": "34EA34*"}, + {"macaddress": "24DFA7*"}, + {"macaddress": "A043B0*"}, + {"macaddress": "B4430D*"} + ] } diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 116c97aeb31..dff7ba6b2fd 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -26,13 +26,14 @@ from homeassistant.components.remote import ( DOMAIN as RM_DOMAIN, PLATFORM_SCHEMA, SERVICE_DELETE_COMMAND, + SERVICE_LEARN_COMMAND, + SERVICE_SEND_COMMAND, SUPPORT_DELETE_COMMAND, SUPPORT_LEARN_COMMAND, RemoteEntity, ) from homeassistant.const import CONF_HOST, STATE_OFF from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store @@ -108,12 +109,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Store(hass, CODE_STORAGE_VERSION, f"broadlink_remote_{device.unique_id}_codes"), Store(hass, FLAG_STORAGE_VERSION, f"broadlink_remote_{device.unique_id}_flags"), ) - - loaded = await remote.async_load_storage_files() - if not loaded: - _LOGGER.error("Failed to create '%s Remote' entity: Storage error", device.name) - return - async_add_entities([remote], False) @@ -126,9 +121,11 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): self._coordinator = device.update_manager.coordinator self._code_storage = codes self._flag_storage = flags + self._storage_loaded = False self._codes = {} self._flags = defaultdict(int) self._state = True + self._lock = asyncio.Lock() @property def name(self): @@ -171,47 +168,52 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): "sw_version": self._device.fw_version, } - def get_code(self, command, device): - """Return a code and a boolean indicating a toggle command. + def _extract_codes(self, commands, device=None): + """Extract a list of codes. If the command starts with `b64:`, extract the code from it. - Otherwise, extract the code from the dictionary, using the device - and command as keys. + Otherwise, extract the code from storage, using the command and + device as keys. - You need to change the flag whenever a toggle command is sent - successfully. Use `self._flags[device] ^= 1`. + The codes are returned in sublists. For toggle commands, the + sublist contains two codes that must be sent alternately with + each call. """ - if command.startswith("b64:"): - code, is_toggle_cmd = command[4:], False + code_list = [] + for cmd in commands: + if cmd.startswith("b64:"): + codes = [cmd[4:]] - else: - if device is None: - raise KeyError("You need to specify a device") - - try: - code = self._codes[device][command] - except KeyError as err: - raise KeyError("Command not found") from err - - # For toggle commands, alternate between codes in a list. - if isinstance(code, list): - code = code[self._flags[device]] - is_toggle_cmd = True else: - is_toggle_cmd = False + if device is None: + raise ValueError("You need to specify a device") - try: - return data_packet(code), is_toggle_cmd - except ValueError as err: - raise ValueError("Invalid code") from err + try: + codes = self._codes[device][cmd] + except KeyError as err: + raise ValueError(f"Command not found: {repr(cmd)}") from err + + if isinstance(codes, list): + codes = codes[:] + else: + codes = [codes] + + for idx, code in enumerate(codes): + try: + codes[idx] = data_packet(code) + except ValueError as err: + raise ValueError(f"Invalid code: {repr(code)}") from err + + code_list.append(codes) + return code_list @callback - def get_codes(self): + def _get_codes(self): """Return a dictionary of codes.""" return self._codes @callback - def get_flags(self): + def _get_flags(self): """Return a dictionary of toggle flags. A toggle flag indicates whether the remote should send an @@ -242,16 +244,13 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): self._state = False self.async_write_ha_state() - async def async_load_storage_files(self): - """Load codes and toggle flags from storage files.""" - try: - self._codes.update(await self._code_storage.async_load() or {}) - self._flags.update(await self._flag_storage.async_load() or {}) - - except HomeAssistantError: - return False - - return True + async def _async_load_storage(self): + """Load code and flag storage from disk.""" + # Exception is intentionally not trapped to + # provide feedback if something fails. + self._codes.update(await self._code_storage.async_load() or {}) + self._flags.update(await self._flag_storage.async_load() or {}) + self._storage_loaded = True async def async_send_command(self, command, **kwargs): """Send a list of commands to a device.""" @@ -261,44 +260,53 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): device = kwargs.get(ATTR_DEVICE) repeat = kwargs[ATTR_NUM_REPEATS] delay = kwargs[ATTR_DELAY_SECS] + service = f"{RM_DOMAIN}.{SERVICE_SEND_COMMAND}" if not self._state: _LOGGER.warning( - "remote.send_command canceled: %s entity is turned off", self.entity_id + "%s canceled: %s entity is turned off", service, self.entity_id ) return - should_delay = False + if not self._storage_loaded: + await self._async_load_storage() - for _, cmd in product(range(repeat), commands): - if should_delay: + try: + code_list = self._extract_codes(commands, device) + except ValueError as err: + _LOGGER.error("Failed to call %s: %s", service, err) + raise + + rf_flags = {0xB2, 0xD7} + if not hasattr(self._device.api, "sweep_frequency") and any( + c[0] in rf_flags for codes in code_list for c in codes + ): + err_msg = f"{self.entity_id} doesn't support sending RF commands" + _LOGGER.error("Failed to call %s: %s", service, err_msg) + raise ValueError(err_msg) + + at_least_one_sent = False + for _, codes in product(range(repeat), code_list): + if at_least_one_sent: await asyncio.sleep(delay) - try: - code, is_toggle_cmd = self.get_code(cmd, device) - - except (KeyError, ValueError) as err: - _LOGGER.error("Failed to send '%s': %s", cmd, err) - should_delay = False - continue + if len(codes) > 1: + code = codes[self._flags[device]] + else: + code = codes[0] try: await self._device.async_request(self._device.api.send_data, code) - - except (AuthorizationError, NetworkTimeoutError, OSError) as err: - _LOGGER.error("Failed to send '%s': %s", cmd, err) + except (BroadlinkException, OSError) as err: + _LOGGER.error("Error during %s: %s", service, err) break - except BroadlinkException as err: - _LOGGER.error("Failed to send '%s': %s", cmd, err) - should_delay = False - continue - - should_delay = True - if is_toggle_cmd: + if len(codes) > 1: self._flags[device] ^= 1 + at_least_one_sent = True - self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) + if at_least_one_sent: + self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY) async def async_learn_command(self, **kwargs): """Learn a list of commands from a remote.""" @@ -307,39 +315,50 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): command_type = kwargs[ATTR_COMMAND_TYPE] device = kwargs[ATTR_DEVICE] toggle = kwargs[ATTR_ALTERNATIVE] + service = f"{RM_DOMAIN}.{SERVICE_LEARN_COMMAND}" if not self._state: _LOGGER.warning( - "remote.learn_command canceled: %s entity is turned off", self.entity_id + "%s canceled: %s entity is turned off", service, self.entity_id ) return - if command_type == COMMAND_TYPE_IR: - learn_command = self._async_learn_ir_command - else: - learn_command = self._async_learn_rf_command + if not self._storage_loaded: + await self._async_load_storage() - should_store = False + async with self._lock: + if command_type == COMMAND_TYPE_IR: + learn_command = self._async_learn_ir_command - for command in commands: - try: - code = await learn_command(command) - if toggle: - code = [code, await learn_command(command)] + elif hasattr(self._device.api, "sweep_frequency"): + learn_command = self._async_learn_rf_command - except (AuthorizationError, NetworkTimeoutError, OSError) as err: - _LOGGER.error("Failed to learn '%s': %s", command, err) - break + else: + err_msg = f"{self.entity_id} doesn't support learning RF commands" + _LOGGER.error("Failed to call %s: %s", service, err_msg) + raise ValueError(err_msg) - except BroadlinkException as err: - _LOGGER.error("Failed to learn '%s': %s", command, err) - continue + should_store = False - self._codes.setdefault(device, {}).update({command: code}) - should_store = True + for command in commands: + try: + code = await learn_command(command) + if toggle: + code = [code, await learn_command(command)] - if should_store: - await self._code_storage.async_save(self._codes) + except (AuthorizationError, NetworkTimeoutError, OSError) as err: + _LOGGER.error("Failed to learn '%s': %s", command, err) + break + + except BroadlinkException as err: + _LOGGER.error("Failed to learn '%s': %s", command, err) + continue + + self._codes.setdefault(device, {}).update({command: code}) + should_store = True + + if should_store: + await self._code_storage.async_save(self._codes) async def _async_learn_ir_command(self, command): """Learn an infrared command.""" @@ -464,6 +483,9 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): ) return + if not self._storage_loaded: + await self._async_load_storage() + try: codes = self._codes[device] except KeyError as err: @@ -494,6 +516,6 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): if not codes: del self._codes[device] if self._flags.pop(device, None) is not None: - self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) + self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY) - self._code_storage.async_delay_save(self.get_codes, CODE_SAVE_DELAY) + self._code_storage.async_delay_save(self._get_codes, CODE_SAVE_DELAY) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index af350329e8c..0e42d8c438f 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -8,11 +8,11 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, + SensorEntity, ) from homeassistant.const import CONF_HOST, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from .const import DOMAIN from .helpers import import_device @@ -56,7 +56,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class BroadlinkSensor(Entity): +class BroadlinkSensor(SensorEntity): """Representation of a Broadlink sensor.""" def __init__(self, device, monitored_condition): diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index b4cd43ac493..0a98530c806 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -109,7 +109,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Broadlink switch.""" device = hass.data[DOMAIN].devices[config_entry.entry_id] - if device.api.type in {"RM2", "RM4"}: + if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: platform_data = hass.data[DOMAIN].platforms.get(SWITCH_DOMAIN, {}) user_defined_switches = platform_data.get(device.api.mac, {}) switches = [ @@ -119,12 +119,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif device.api.type == "SP1": switches = [BroadlinkSP1Switch(device)] - elif device.api.type == "SP2": + elif device.api.type in {"SP2", "SP2S", "SP3", "SP3S", "SP4", "SP4B"}: switches = [BroadlinkSP2Switch(device)] - elif device.api.type in {"SP4", "SP4B"}: - switches = [BroadlinkSP4Switch(device)] - elif device.api.type == "BG1": switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)] @@ -143,7 +140,6 @@ class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): self._command_on = command_on self._command_off = command_off self._coordinator = device.update_manager.coordinator - self._device_class = None self._state = None @property @@ -174,7 +170,7 @@ class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): @property def device_class(self): """Return device class.""" - return self._device_class + return DEVICE_CLASS_SWITCH @property def device_info(self): @@ -254,7 +250,6 @@ class BroadlinkSP1Switch(BroadlinkSwitch): def __init__(self, device): """Initialize the switch.""" super().__init__(device, 1, 0) - self._device_class = DEVICE_CLASS_OUTLET @property def unique_id(self): @@ -277,10 +272,8 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): def __init__(self, device, *args, **kwargs): """Initialize the switch.""" super().__init__(device, *args, **kwargs) - self._state = self._coordinator.data["state"] - self._load_power = self._coordinator.data["load_power"] - if device.api.model == "SC1": - self._device_class = DEVICE_CLASS_SWITCH + self._state = self._coordinator.data["pwr"] + self._load_power = self._coordinator.data.get("power") @property def assumed_state(self): @@ -292,33 +285,12 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): """Return the current power usage in Watt.""" return self._load_power - @callback - def update_data(self): - """Update data.""" - if self._coordinator.last_update_success: - self._state = self._coordinator.data["state"] - self._load_power = self._coordinator.data["load_power"] - self.async_write_ha_state() - - -class BroadlinkSP4Switch(BroadlinkSP1Switch): - """Representation of a Broadlink SP4 switch.""" - - def __init__(self, device, *args, **kwargs): - """Initialize the switch.""" - super().__init__(device, *args, **kwargs) - self._state = self._coordinator.data["pwr"] - - @property - def assumed_state(self): - """Return True if unable to access real state of the switch.""" - return False - @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: self._state = self._coordinator.data["pwr"] + self._load_power = self._coordinator.data.get("power") self.async_write_ha_state() @@ -330,7 +302,6 @@ class BroadlinkMP1Slot(BroadlinkSwitch): super().__init__(device, 1, 0) self._slot = slot self._state = self._coordinator.data[f"s{slot}"] - self._device_class = DEVICE_CLASS_OUTLET @property def unique_id(self): @@ -374,7 +345,6 @@ class BroadlinkBG1Slot(BroadlinkSwitch): super().__init__(device, 1, 0) self._slot = slot self._state = self._coordinator.data[f"pwr{slot}"] - self._device_class = DEVICE_CLASS_OUTLET @property def unique_id(self): @@ -391,6 +361,11 @@ class BroadlinkBG1Slot(BroadlinkSwitch): """Return True if unable to access real state of the switch.""" return False + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_OUTLET + @callback def update_data(self): """Update data.""" diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index 3b2d79a34a7..90213e99aec 100644 --- a/homeassistant/components/broadlink/translations/hu.json +++ b/homeassistant/components/broadlink/translations/hu.json @@ -1,7 +1,46 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name} ({model} a {host} c\u00edmen)", + "step": { + "auth": { + "title": "Hiteles\u00edt\u00e9s az eszk\u00f6z\u00f6n" + }, + "finish": { + "data": { + "name": "N\u00e9v" + }, + "title": "V\u00e1lassz egy nevet az eszk\u00f6znek" + }, + "reset": { + "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. A hiteles\u00edt\u00e9shez \u00e9s a konfigur\u00e1ci\u00f3 befejez\u00e9s\u00e9hez fel kell oldani az eszk\u00f6z z\u00e1rol\u00e1s\u00e1t. Utas\u00edt\u00e1sok:\n 1. Nyisd meg a Broadlink alkalmaz\u00e1st.\n 2. Kattints az eszk\u00f6zre.\n 3. Kattints a jobb fels\u0151 sarokban tal\u00e1lhat\u00f3 `...` gombra.\n 4. G\u00f6rgess az oldal alj\u00e1ra.\n 5. Kapcsold ki a z\u00e1rol\u00e1s\u00e1t.", + "title": "Az eszk\u00f6z felold\u00e1sa" + }, + "unlock": { + "data": { + "unlock": "Igen, csin\u00e1ld." + }, + "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. Ez hiteles\u00edt\u00e9si probl\u00e9m\u00e1khoz vezethet a Home Assistantban. Szeretn\u00e9d feloldani?", + "title": "Az eszk\u00f6z felold\u00e1sa (opcion\u00e1lis)" + }, + "user": { + "data": { + "host": "Hoszt", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s" + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + } } } } \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/id.json b/homeassistant/components/broadlink/translations/id.json new file mode 100644 index 00000000000..89d9b17a800 --- /dev/null +++ b/homeassistant/components/broadlink/translations/id.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "not_supported": "Perangkat tidak didukung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name} ({model} di {host})", + "step": { + "auth": { + "title": "Autentikasi ke perangkat" + }, + "finish": { + "data": { + "name": "Nama" + }, + "title": "Pilih nama untuk perangkat" + }, + "reset": { + "description": "{name} ({model} di {host}) dikunci. Anda perlu membuka kunci perangkat untuk mengautentikasi dan menyelesaikan konfigurasi. Ikuti petunjuk berikut:\n1. Buka aplikasi Broadlink.\n2. Klik pada perangkat.\n3. Klik `\u2026` di pojok kanan atas.\n4. Gulir ke bagian bawah halaman.\n5. Nonaktifkan kunci.", + "title": "Buka kunci perangkat" + }, + "unlock": { + "data": { + "unlock": "Ya, lakukan." + }, + "description": "{name} ({model} di {host}) dikunci. Hal ini dapat menyebabkan masalah autentikasi di Home Assistant. Apakah Anda ingin membukanya?", + "title": "Buka kunci perangkat (opsional)" + }, + "user": { + "data": { + "host": "Host", + "timeout": "Tenggang waktu" + }, + "title": "Hubungkan ke perangkat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/ko.json b/homeassistant/components/broadlink/translations/ko.json index 13cd17a8475..ac3ada0b831 100644 --- a/homeassistant/components/broadlink/translations/ko.json +++ b/homeassistant/components/broadlink/translations/ko.json @@ -4,42 +4,43 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "not_supported": "\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uc7a5\uce58", + "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_supported": "\uae30\uae30\uac00 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\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_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \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", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "{name} ({host} \uc758 {model})", + "flow_title": "{name} ({host}\uc758 {model})", "step": { "auth": { - "title": "\uc7a5\uce58\uc5d0 \uc778\uc99d" + "title": "\uae30\uae30\uc5d0 \uc778\uc99d\ud558\uae30" }, "finish": { "data": { "name": "\uc774\ub984" }, - "title": "\uc7a5\uce58 \uc774\ub984\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624" + "title": "\uae30\uae30\uc5d0 \ub300\ud55c \uc774\ub984 \uc120\ud0dd\ud558\uae30" }, "reset": { - "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c" + "description": "{name} ({host}\uc758 {model})\uc774(\uac00) \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc744 \uc778\uc99d\ud558\uace0 \uc644\ub8cc\ud558\ub824\uba74 \uae30\uae30\uc758 \uc7a0\uae08\uc744 \ud574\uc81c\ud574\uc57c \ud569\ub2c8\ub2e4. \ub2e4\uc74c\uc744 \ub530\ub77c\uc8fc\uc138\uc694:\n1. Broadlink \uc571\uc744 \uc5f4\uc5b4\uc8fc\uc138\uc694.\n2. \uae30\uae30\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n3. \uc624\ub978\ucabd \uc0c1\ub2e8\uc758 `...`\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n4. \ud398\uc774\uc9c0 \ub9e8 \uc544\ub798\ub85c \uc2a4\ud06c\ub864\ud574\uc8fc\uc138\uc694.\n5. \uc7a0\uae08\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.", + "title": "\uae30\uae30 \uc7a0\uae08 \ud574\uc81c\ud558\uae30" }, "unlock": { "data": { - "unlock": "\uc608" + "unlock": "\uc608, \uc7a0\uae08 \ud574\uc81c\ud558\uaca0\uc2b5\ub2c8\ub2e4." }, - "description": "{name} ({host} \uc758 {model}) \uc774(\uac00) \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant \uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uc7a5\uce58 \uc7a0\uae08 \ud574\uc81c (\uc635\uc158)" + "description": "{name} ({host}\uc758 {model})\uc774(\uac00) \uc7a0\uaca8 \uc788\uc2b5\ub2c8\ub2e4. \uc774\ub85c \uc778\ud574 Home Assistant\uc5d0\uc11c \uc778\uc99d \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc7a0\uae08\uc744 \ud574\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uae30\uae30 \uc7a0\uae08 \ud574\uc81c\ud558\uae30 (\uc120\ud0dd \uc0ac\ud56d)" }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", "timeout": "\uc81c\ud55c \uc2dc\uac04" }, - "title": "\uc7a5\uce58\uc5d0 \uc5f0\uacb0" + "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index d2db5476555..06c26235d0a 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -15,6 +15,9 @@ }, "flow_title": "{name} ({model} bij {host})", "step": { + "auth": { + "title": "Authenticeer naar het apparaat" + }, "finish": { "data": { "name": "Naam" @@ -22,16 +25,20 @@ "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.", "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?", + "title": "Ontgrendel het apparaat (optioneel)" }, "user": { "data": { - "host": "Host" + "host": "Host", + "timeout": "Time-out" }, "title": "Verbinding maken met het apparaat" } diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index c9b273218b5..8401dba8c0d 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -1,17 +1,9 @@ """Support for fetching data from Broadlink devices.""" from abc import ABC, abstractmethod from datetime import timedelta -from functools import partial import logging -import broadlink as blk -from broadlink.exceptions import ( - AuthorizationError, - BroadlinkException, - CommandNotSupportedError, - NetworkTimeoutError, - StorageError, -) +from broadlink.exceptions import AuthorizationError, BroadlinkException from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt @@ -21,17 +13,20 @@ _LOGGER = logging.getLogger(__name__) def get_update_manager(device): """Return an update manager for a given Broadlink device.""" - if device.api.model.startswith("RM mini"): - return BroadlinkRMMini3UpdateManager(device) - update_managers = { "A1": BroadlinkA1UpdateManager, "BG1": BroadlinkBG1UpdateManager, "MP1": BroadlinkMP1UpdateManager, - "RM2": BroadlinkRMUpdateManager, - "RM4": BroadlinkRMUpdateManager, + "RM4MINI": BroadlinkRMUpdateManager, + "RM4PRO": BroadlinkRMUpdateManager, + "RMMINI": BroadlinkRMUpdateManager, + "RMMINIB": BroadlinkRMUpdateManager, + "RMPRO": BroadlinkRMUpdateManager, "SP1": BroadlinkSP1UpdateManager, "SP2": BroadlinkSP2UpdateManager, + "SP2S": BroadlinkSP2UpdateManager, + "SP3": BroadlinkSP2UpdateManager, + "SP3S": BroadlinkSP2UpdateManager, "SP4": BroadlinkSP4UpdateManager, "SP4B": BroadlinkSP4UpdateManager, } @@ -114,28 +109,18 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager): return await self.device.async_request(self.device.api.check_power) -class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager): - """Manages updates for Broadlink RM mini 3 devices.""" - - async def async_fetch_data(self): - """Fetch data from the device.""" - hello = partial( - blk.discover, - discover_ip_address=self.device.api.host[0], - timeout=self.device.api.timeout, - ) - devices = await self.device.hass.async_add_executor_job(hello) - if not devices: - raise NetworkTimeoutError("The device is offline") - return {} - - class BroadlinkRMUpdateManager(BroadlinkUpdateManager): - """Manages updates for Broadlink RM2 and RM4 devices.""" + """Manages updates for Broadlink remotes.""" async def async_fetch_data(self): """Fetch data from the device.""" - return await self.device.async_request(self.device.api.check_sensors) + device = self.device + + if hasattr(device.api, "check_sensors"): + return await device.async_request(device.api.check_sensors) + + await device.async_request(device.api.update) + return {} class BroadlinkSP1UpdateManager(BroadlinkUpdateManager): @@ -151,14 +136,14 @@ class BroadlinkSP2UpdateManager(BroadlinkUpdateManager): async def async_fetch_data(self): """Fetch data from the device.""" + device = self.device + data = {} - data["state"] = await self.device.async_request(self.device.api.check_power) - try: - data["load_power"] = await self.device.async_request( - self.device.api.get_energy - ) - except (CommandNotSupportedError, StorageError): - data["load_power"] = None + data["pwr"] = await device.async_request(device.api.check_power) + + if hasattr(device.api, "get_energy"): + data["power"] = await device.async_request(device.api.get_energy) + return data diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index d7cf906a87c..cd5a8b444b3 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -7,8 +7,7 @@ from brother import Brother, SnmpError, UnsupportedModel from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE -from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP @@ -21,11 +20,6 @@ SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: Config): - """Set up the Brother component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Brother from a config entry.""" host = entry.data[CONF_HOST] @@ -36,19 +30,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = BrotherDataUpdateCoordinator( hass, host=host, kind=kind, snmp_engine=snmp_engine ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {}) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator hass.data[DOMAIN][SNMP] = snmp_engine - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -59,8 +50,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 49f1c0ed1a3..1d635984b72 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST, CONF_TYPE -from .const import DOMAIN, PRINTER_TYPES # pylint:disable=unused-import +from .const import DOMAIN, PRINTER_TYPES from .utils import get_snmp_engine DATA_SCHEMA = vol.Schema( diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 15828e5f05a..13933b7bf60 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.2.1"], + "requirements": ["brother==0.2.2"], "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], "config_flow": true, "quality_scale": "platinum" diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index a379d9b4154..0b614ffa582 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,4 +1,5 @@ """Support for the Brother service.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -56,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class BrotherPrinterSensor(CoordinatorEntity): +class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Define an Brother Printer sensor.""" def __init__(self, coordinator, kind, device_info): @@ -88,7 +89,7 @@ class BrotherPrinterSensor(CoordinatorEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" remaining_pages = None drum_counter = None diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json index dd5711cc516..ae950f58f72 100644 --- a/homeassistant/components/brother/translations/hu.json +++ b/homeassistant/components/brother/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ez a nyomtat\u00f3 m\u00e1r konfigur\u00e1lva van.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott." }, "error": { diff --git a/homeassistant/components/brother/translations/id.json b/homeassistant/components/brother/translations/id.json new file mode 100644 index 00000000000..5e0b562017c --- /dev/null +++ b/homeassistant/components/brother/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "unsupported_model": "Model printer ini tidak didukung." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "snmp_error": "Server SNMP dimatikan atau printer tidak didukung.", + "wrong_host": "Nama host atau alamat IP tidak valid." + }, + "flow_title": "Printer Brother: {model} {serial_number}", + "step": { + "user": { + "data": { + "host": "Host", + "type": "Jenis printer" + }, + "description": "Siapkan integrasi printer Brother. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Jenis printer" + }, + "description": "Ingin menambahkan Printer Brother {model} dengan nomor seri `{serial_number}` ke Home Assistant?", + "title": "Perangkat Printer Brother yang Ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/ko.json b/homeassistant/components/brother/translations/ko.json index 47722afdae5..2d3b587e475 100644 --- a/homeassistant/components/brother/translations/ko.json +++ b/homeassistant/components/brother/translations/ko.json @@ -22,7 +22,7 @@ "data": { "type": "\ud504\ub9b0\ud130\uc758 \uc885\ub958" }, - "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \ub85c \ube0c\ub77c\ub354 \ud504\ub9b0\ud130 {model} \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}`\uc758 {model} \ube0c\ub77c\ub354 \ud504\ub9b0\ud130\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ubc1c\uacac\ub41c \ube0c\ub77c\ub354 \ud504\ub9b0\ud130" } } diff --git a/homeassistant/components/brother/translations/nl.json b/homeassistant/components/brother/translations/nl.json index d754b2df9c1..531038d827b 100644 --- a/homeassistant/components/brother/translations/nl.json +++ b/homeassistant/components/brother/translations/nl.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Printerhostnaam of IP-adres", + "host": "Host", "type": "Type printer" }, "description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother" diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 7b2c3e585e3..32af96dfe60 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -7,7 +7,7 @@ import uuid import brottsplatskartan import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_NAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -78,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([BrottsplatskartanSensor(bpk, name)], True) -class BrottsplatskartanSensor(Entity): +class BrottsplatskartanSensor(SensorEntity): """Representation of a Brottsplatskartan Sensor.""" def __init__(self, bpk, name): @@ -99,7 +98,7 @@ class BrottsplatskartanSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index ceb56ba03fa..9c539fe51fe 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -131,7 +131,7 @@ class BruntDevice(CoverEntity): return self.move_state == 2 @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the detailed device state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index bab4af29422..f452451050b 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -9,18 +9,12 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN SCAN_INTERVAL = timedelta(seconds=30) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the BSB-Lan component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BSB-Lan from a config entry.""" diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index a97c13c3424..4d83fb04dbe 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -1,7 +1,9 @@ """BSBLAN platform to control a compatible Climate Device.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable from bsblan import BSBLan, BSBLanError, Info, State @@ -74,7 +76,7 @@ BSBLAN_TO_HA_PRESET = { async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up BSBLan device based on a config entry.""" bsblan: BSBLan = hass.data[DOMAIN][entry.entry_id][DATA_BSBLAN_CLIENT] @@ -92,10 +94,10 @@ class BSBLanClimate(ClimateEntity): info: Info, ): """Initialize BSBLan climate device.""" - self._current_temperature: Optional[float] = None + self._current_temperature: float | None = None self._available = True - self._hvac_mode: Optional[str] = None - self._target_temperature: Optional[float] = None + self._hvac_mode: str | None = None + self._target_temperature: float | None = None self._temperature_unit = None self._preset_mode = None self._store_hvac_mode = None @@ -229,7 +231,7 @@ class BSBLanClimate(ClimateEntity): self._temperature_unit = state.current_temperature.unit @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this BSBLan device.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)}, diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index dee04e6ef85..f5df1df0437 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -1,6 +1,8 @@ """Config flow for BSB-Lan integration.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from bsblan import BSBLan, BSBLanError, Info import voluptuous as vol @@ -10,11 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from .const import ( # pylint:disable=unused-import - CONF_DEVICE_IDENT, - CONF_PASSKEY, - DOMAIN, -) +from .const import CONF_DEVICE_IDENT, CONF_PASSKEY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -26,8 +24,8 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -59,7 +57,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -78,9 +76,9 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN): async def _get_bsblan_info( self, host: str, - username: Optional[str], - password: Optional[str], - passkey: Optional[str], + username: str | None, + password: str | None, + passkey: str | None, port: int, ) -> Info: """Get device information from an BSBLan device.""" diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index 1d28556ba1a..50d250cc384 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -1,13 +1,19 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "BSB-Lan: {name}", "step": { "user": { "data": { "host": "Hoszt", - "port": "Port" + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } diff --git a/homeassistant/components/bsblan/translations/id.json b/homeassistant/components/bsblan/translations/id.json new file mode 100644 index 00000000000..6e8ac0bd4cb --- /dev/null +++ b/homeassistant/components/bsblan/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "passkey": "String kunci sandi", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "description": "Siapkan perangkat BSB-Lan Anda untuk diintegrasikan dengan Home Assistant.", + "title": "Hubungkan ke perangkat BSB-Lan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/ko.json b/homeassistant/components/bsblan/translations/ko.json index 85703c7eeb7..65843a25833 100644 --- a/homeassistant/components/bsblan/translations/ko.json +++ b/homeassistant/components/bsblan/translations/ko.json @@ -16,7 +16,7 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "Home Assistant \uc5d0 BSB-Lan \uae30\uae30 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "description": "Home Assistant\uc5d0 BSB-Lan \uae30\uae30 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", "title": "BSB-Lan \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json index 76aa715a9de..8291a20d307 100644 --- a/homeassistant/components/bsblan/translations/ru.json +++ b/homeassistant/components/bsblan/translations/ru.json @@ -14,7 +14,7 @@ "passkey": "\u041f\u0430\u0440\u043e\u043b\u044c", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BSB-Lan.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 44f86589b27..92f25b7ffc6 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -1,8 +1,9 @@ """Provide animated GIF loops of Buienradar imagery.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta import logging -from typing import Optional import aiohttp import voluptuous as vol @@ -85,13 +86,13 @@ class BuienradarCam(Camera): # invariant: this condition is private to and owned by this instance. self._condition = asyncio.Condition() - self._last_image: Optional[bytes] = None + self._last_image: bytes | None = None # value of the last seen last modified header - self._last_modified: Optional[str] = None + self._last_modified: str | None = None # loading status self._loading = False # deadline for image refresh - self.delta after last successful load - self._deadline: Optional[datetime] = None + self._deadline: datetime | None = None self._unique_id = f"{self._dimension}_{self._country}" @@ -140,7 +141,7 @@ class BuienradarCam(Camera): _LOGGER.error("Failed to fetch image, %s", type(err)) return False - async def async_camera_image(self) -> Optional[bytes]: + async def async_camera_image(self) -> bytes | None: """ Return a still image response from the camera. diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 4e35542b581..170493969f8 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -20,7 +20,7 @@ from buienradar.constants import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, @@ -39,7 +39,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util from .const import DEFAULT_TIMEFRAME @@ -236,7 +235,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await data.schedule_update(1) -class BrSensor(Entity): +class BrSensor(SensorEntity): """Representation of an Buienradar sensor.""" def __init__(self, sensor_type, client_name, coordinates): @@ -309,7 +308,7 @@ class BrSensor(Entity): try: condition = data.get(FORECAST)[fcday].get(CONDITION) except IndexError: - _LOGGER.warning("No forecast for fcday=%s...", fcday) + _LOGGER.warning("No forecast for fcday=%s", fcday) return False if condition: @@ -339,7 +338,7 @@ class BrSensor(Entity): self._state = round(self._state * 3.6, 1) return True except IndexError: - _LOGGER.warning("No forecast for fcday=%s...", fcday) + _LOGGER.warning("No forecast for fcday=%s", fcday) return False # update all other sensors @@ -347,7 +346,7 @@ class BrSensor(Entity): self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) return True except IndexError: - _LOGGER.warning("No forecast for fcday=%s...", fcday) + _LOGGER.warning("No forecast for fcday=%s", fcday) return False if self.type == SYMBOL or self.type.startswith(CONDITION): @@ -430,7 +429,7 @@ class BrSensor(Entity): return self._entity_picture @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: self._attribution} diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index b4f2314eee5..83c511713d0 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -82,7 +82,7 @@ class BrData: async def get_data(self, url): """Load data from specified url.""" - _LOGGER.debug("Calling url: %s...", url) + _LOGGER.debug("Calling url: %s", url) result = {SUCCESS: False, MESSAGE: None} resp = None try: diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 4b0391c3190..2ff638a2550 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -201,7 +201,7 @@ class BrWeather(WeatherEntity): # keys understood by the weather component: condcode = data_in.get(CONDITION, []).get(CONDCODE) data_out = { - ATTR_FORECAST_TIME: data_in.get(DATETIME), + ATTR_FORECAST_TIME: data_in.get(DATETIME).isoformat(), ATTR_FORECAST_CONDITION: cond[condcode], ATTR_FORECAST_TEMP_LOW: data_in.get(MIN_TEMP), ATTR_FORECAST_TEMP: data_in.get(MAX_TEMP), diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 66b3c974306..62be361df3b 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -123,7 +123,7 @@ class WebDavCalendarEventDevice(CalendarEventDevice): self._offset_reached = False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return {"offset_reached": self._offset_reached} diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 478dafb3423..11a6916ba83 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,8 +1,10 @@ """Support for Google Calendar event device sensors.""" +from __future__ import annotations + from datetime import timedelta import logging import re -from typing import Dict, List, cast +from typing import cast, final from aiohttp import web @@ -127,13 +129,14 @@ def is_offset_reached(event): class CalendarEventDevice(Entity): - """A calendar event device.""" + """Base class for calendar event entities.""" @property def event(self): """Return the next upcoming event.""" raise NotImplementedError() + @final @property def state_attributes(self): """Return the entity state attributes.""" @@ -218,7 +221,7 @@ class CalendarListView(http.HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Retrieve calendar list.""" hass = request.app["hass"] - calendar_list: List[Dict[str, str]] = [] + calendar_list: list[dict[str, str]] = [] for entity in self.component.entities: state = hass.states.get(entity.entity_id) diff --git a/homeassistant/components/calendar/translations/id.json b/homeassistant/components/calendar/translations/id.json index 383a6ba77a1..e48c6e69b98 100644 --- a/homeassistant/components/calendar/translations/id.json +++ b/homeassistant/components/calendar/translations/id.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Kalender" diff --git a/homeassistant/components/calendar/translations/ko.json b/homeassistant/components/calendar/translations/ko.json index af8622be7d7..fd1672fef56 100644 --- a/homeassistant/components/calendar/translations/ko.json +++ b/homeassistant/components/calendar/translations/ko.json @@ -5,5 +5,5 @@ "on": "\ucf1c\uc9d0" } }, - "title": "\uc77c\uc815" + "title": "\uce98\ub9b0\ub354" } \ No newline at end of file diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 99b5cebc2a3..70739857587 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -8,6 +8,7 @@ import hashlib import logging import os from random import SystemRandom +from typing import final from aiohttp import web import async_timeout @@ -441,6 +442,7 @@ class Camera(Entity): """Call the job and disable motion detection.""" await self.hass.async_add_executor_job(self.disable_motion_detection) + @final @property def state_attributes(self): """Return the camera state attributes.""" diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 64f2d00735e..ca6c4118753 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -95,10 +95,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool raise ConfigEntryNotReady from error coordinator = CanaryDataUpdateCoordinator(hass, api=canary_api) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() undo_listener = entry.add_update_listener(_async_update_listener) @@ -107,9 +104,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -120,8 +117,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index e4dda0f9f33..933e6708e22 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,5 +1,7 @@ """Support for Canary alarm.""" -from typing import Callable, List +from __future__ import annotations + +from typing import Callable from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT @@ -27,7 +29,7 @@ from .coordinator import CanaryDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Canary alarm control panels based on a config entry.""" coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ @@ -87,7 +89,7 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"private": self.location.is_private} diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 0493a964cc4..703ae2edc8a 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -1,7 +1,9 @@ """Support for Canary camera.""" +from __future__ import annotations + import asyncio from datetime import timedelta -from typing import Callable, List +from typing import Callable from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame @@ -44,7 +46,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Canary sensors based on a config entry.""" coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index dc2822d836a..2d324e09cc8 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Canary.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from canary.api import Api from requests import ConnectTimeout, HTTPError @@ -11,13 +13,17 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_TIMEOUT -from .const import DOMAIN # pylint: disable=unused-import +from .const import ( + CONF_FFMPEG_ARGUMENTS, + DEFAULT_FFMPEG_ARGUMENTS, + DEFAULT_TIMEOUT, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: +def validate_input(hass: HomeAssistantType, 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. @@ -45,14 +51,14 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): return CanaryOptionsFlowHandler(config_entry) async def async_step_import( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -100,7 +106,7 @@ class CanaryOptionsFlowHandler(OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: Optional[ConfigType] = None): + async def async_step_init(self, user_input: ConfigType | None = None): """Manage Canary options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 99dcdf48fce..d7a6648857a 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,8 +1,11 @@ """Support for Canary sensors.""" -from typing import Callable, List +from __future__ import annotations + +from typing import Callable from canary.api import SensorType +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -54,7 +57,7 @@ STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Canary sensors based on a config entry.""" coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ @@ -75,7 +78,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class CanarySensor(CoordinatorEntity, Entity): +class CanarySensor(CoordinatorEntity, SensorEntity): """Representation of a Canary sensor.""" def __init__(self, coordinator, sensor_type, location, device): @@ -163,7 +166,7 @@ class CanarySensor(CoordinatorEntity, Entity): return self._sensor_type[2] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" reading = self.reading diff --git a/homeassistant/components/canary/translations/hu.json b/homeassistant/components/canary/translations/hu.json new file mode 100644 index 00000000000..c2c70fdbf22 --- /dev/null +++ b/homeassistant/components/canary/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Csatlakoz\u00e1s a Canary-hoz" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (opcion\u00e1lis)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/id.json b/homeassistant/components/canary/translations/id.json new file mode 100644 index 00000000000..5f092847b4d --- /dev/null +++ b/homeassistant/components/canary/translations/id.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke Canary" + } + } + }, + "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/canary/translations/ko.json b/homeassistant/components/canary/translations/ko.json index d02344a9027..c96049f16ff 100644 --- a/homeassistant/components/canary/translations/ko.json +++ b/homeassistant/components/canary/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -14,7 +14,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "Canary\uc5d0 \uc5f0\uacb0" + "title": "Canary\uc5d0 \uc5f0\uacb0\ud558\uae30" } } }, @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "ffmpeg_arguments": "\uce74\uba54\ub77c ffmpeg\uc5d0 \uc804\ub2ec \ub41c \uc778\uc218", + "ffmpeg_arguments": "\uce74\uba54\ub77c\uc5d0 \ub300\ud55c ffmpeg \uc804\ub2ec \uc778\uc218", "timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)" } } diff --git a/homeassistant/components/canary/translations/nl.json b/homeassistant/components/canary/translations/nl.json index 9681bcd7c37..fbe642bbc96 100644 --- a/homeassistant/components/canary/translations/nl.json +++ b/homeassistant/components/canary/translations/nl.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "Argumenten doorgegeven aan ffmpeg voor camera's", "timeout": "Time-out verzoek (seconden)" } } diff --git a/homeassistant/components/canary/translations/ru.json b/homeassistant/components/canary/translations/ru.json index 146863cf768..51052d0d68d 100644 --- a/homeassistant/components/canary/translations/ru.json +++ b/homeassistant/components/canary/translations/ru.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 Canary" } diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 49cec207764..43b6b77ebd2 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,20 +1,42 @@ """Component to embed Google Cast.""" +import logging + +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.helpers import config_validation as cv from . import home_assistant_cast from .const import DOMAIN +from .media_player import ENTITY_SCHEMA + +# Deprecated from 2021.4, remove in 2021.6 +CONFIG_SCHEMA = cv.deprecated(DOMAIN) + +_LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up the Cast component.""" conf = config.get(DOMAIN) - hass.data[DOMAIN] = conf or {} - if conf is not None: + media_player_config_validated = [] + media_player_config = conf.get("media_player", {}) + if not isinstance(media_player_config, list): + media_player_config = [media_player_config] + for cfg in media_player_config: + try: + cfg = ENTITY_SCHEMA(cfg) + media_player_config_validated.append(cfg) + except vol.Error as ex: + _LOGGER.warning("Invalid config '%s': %s", cfg, ex) + hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=media_player_config_validated, ) ) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index e00048a7589..464283e07f3 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,35 +1,180 @@ """Config flow for Cast.""" -import functools - -from pychromecast.discovery import discover_chromecasts, stop_discovery +import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.helpers import config_entry_flow +from homeassistant.helpers import config_validation as cv -from .const import DOMAIN -from .helpers import ChromeCastZeroconf +from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, CONF_UUID, DOMAIN + +IGNORE_CEC_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string])) +KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string])) +WANTED_UUID_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string])) -async def _async_has_devices(hass): - """ - Return if there are devices that can be discovered. +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" - This function will be called if no devices are already found through the zeroconf - integration. - """ + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH - zeroconf_instance = ChromeCastZeroconf.get_zeroconf() - if zeroconf_instance is None: - zeroconf_instance = await zeroconf.async_get_instance(hass) + def __init__(self): + """Initialize flow.""" + self._ignore_cec = set() + self._known_hosts = set() + self._wanted_uuid = set() - casts, browser = await hass.async_add_executor_job( - functools.partial(discover_chromecasts, zeroconf_instance=zeroconf_instance) - ) - stop_discovery(browser) - return casts + @staticmethod + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return CastOptionsFlowHandler(config_entry) + + async def async_step_import(self, import_data=None): + """Import data.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + media_player_config = import_data or [] + for cfg in media_player_config: + if CONF_IGNORE_CEC in cfg: + self._ignore_cec.update(set(cfg[CONF_IGNORE_CEC])) + if CONF_UUID in cfg: + self._wanted_uuid.add(cfg[CONF_UUID]) + + data = self._get_data() + return self.async_create_entry(title="Google Cast", data=data) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_config() + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initialized by zeroconf discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(DOMAIN) + + return await self.async_step_confirm() + + async def async_step_config(self, user_input=None): + """Confirm the setup.""" + errors = {} + data = {CONF_KNOWN_HOSTS: self._known_hosts} + + if user_input is not None: + bad_hosts = False + known_hosts = user_input[CONF_KNOWN_HOSTS] + known_hosts = [x.strip() for x in known_hosts.split(",") if x.strip()] + try: + known_hosts = KNOWN_HOSTS_SCHEMA(known_hosts) + except vol.Invalid: + errors["base"] = "invalid_known_hosts" + bad_hosts = True + else: + self._known_hosts = known_hosts + data = self._get_data() + if not bad_hosts: + return self.async_create_entry(title="Google Cast", data=data) + + fields = {} + fields[vol.Optional(CONF_KNOWN_HOSTS, default="")] = str + + return self.async_show_form( + step_id="config", data_schema=vol.Schema(fields), errors=errors + ) + + async def async_step_confirm(self, user_input=None): + """Confirm the setup.""" + + data = self._get_data() + + if user_input is not None: + return self.async_create_entry(title="Google Cast", data=data) + + return self.async_show_form(step_id="confirm") + + def _get_data(self): + return { + CONF_IGNORE_CEC: list(self._ignore_cec), + CONF_KNOWN_HOSTS: list(self._known_hosts), + CONF_UUID: list(self._wanted_uuid), + } -config_entry_flow.register_discovery_flow( - DOMAIN, "Google Cast", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH -) +class CastOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Google Cast options.""" + + def __init__(self, config_entry): + """Initialize MQTT options flow.""" + self.config_entry = config_entry + self.broker_config = {} + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Cast options.""" + return await self.async_step_options() + + async def async_step_options(self, user_input=None): + """Manage the MQTT options.""" + errors = {} + current_config = self.config_entry.data + if user_input is not None: + bad_cec, ignore_cec = _string_to_list( + user_input.get(CONF_IGNORE_CEC, ""), IGNORE_CEC_SCHEMA + ) + bad_hosts, known_hosts = _string_to_list( + user_input.get(CONF_KNOWN_HOSTS, ""), KNOWN_HOSTS_SCHEMA + ) + bad_uuid, wanted_uuid = _string_to_list( + user_input.get(CONF_UUID, ""), WANTED_UUID_SCHEMA + ) + + if not bad_cec and not bad_hosts and not bad_uuid: + updated_config = {} + updated_config[CONF_IGNORE_CEC] = ignore_cec + updated_config[CONF_KNOWN_HOSTS] = known_hosts + updated_config[CONF_UUID] = wanted_uuid + self.hass.config_entries.async_update_entry( + self.config_entry, data=updated_config + ) + return self.async_create_entry(title="", data=None) + + fields = {} + suggested_value = _list_to_string(current_config.get(CONF_KNOWN_HOSTS)) + _add_with_suggestion(fields, CONF_KNOWN_HOSTS, suggested_value) + if self.show_advanced_options: + suggested_value = _list_to_string(current_config.get(CONF_UUID)) + _add_with_suggestion(fields, CONF_UUID, suggested_value) + suggested_value = _list_to_string(current_config.get(CONF_IGNORE_CEC)) + _add_with_suggestion(fields, CONF_IGNORE_CEC, suggested_value) + + return self.async_show_form( + step_id="options", + data_schema=vol.Schema(fields), + errors=errors, + ) + + +def _list_to_string(items): + comma_separated_string = "" + if items: + comma_separated_string = ",".join(items) + return comma_separated_string + + +def _string_to_list(string, schema): + invalid = False + items = [x.strip() for x in string.split(",") if x.strip()] + try: + items = schema(items) + except vol.Invalid: + invalid = True + + return invalid, items + + +def _add_with_suggestion(fields, key, suggested_value): + fields[vol.Optional(key, description={"suggested_value": suggested_value})] = str diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index c6164484dbb..03ffdfbd15c 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -5,14 +5,13 @@ DEFAULT_PORT = 8009 # Stores a threading.Lock that is held by the internal pychromecast discovery. INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" -# Stores all ChromecastInfo we encountered through discovery or config as a set -# If we find a chromecast with a new host, the old one will be removed again. -KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" # Stores UUIDs of cast devices that were added as entities. Doesn't store # None UUIDs. ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" # Stores an audio group manager. CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" +# Store a CastBrowser +CAST_BROWSER_KEY = "cast_browser" # Dispatcher signal fired with a ChromecastInfo every time we discover a new # Chromecast or receive it through configuration @@ -24,3 +23,7 @@ SIGNAL_CAST_REMOVED = "cast_removed" # Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view. SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view" + +CONF_IGNORE_CEC = "ignore_cec" +CONF_KNOWN_HOSTS = "known_hosts" +CONF_UUID = "uuid" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 4858d37f732..a5ac4c02047 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -9,8 +9,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + CAST_BROWSER_KEY, + CONF_KNOWN_HOSTS, + DEFAULT_PORT, INTERNAL_DISCOVERY_RUNNING_KEY, - KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, ) @@ -19,19 +21,24 @@ from .helpers import ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) -def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): +def discover_chromecast(hass: HomeAssistant, device_info): """Discover a Chromecast.""" + + info = ChromecastInfo( + services=device_info.services, + uuid=device_info.uuid, + model_name=device_info.model_name, + friendly_name=device_info.friendly_name, + is_audio_group=device_info.port != DEFAULT_PORT, + ) + if info.uuid is None: _LOGGER.error("Discovered chromecast without uuid %s", info) return info = info.fill_out_missing_chromecast_info() - if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]: - _LOGGER.debug("Discovered update for known chromecast %s", info) - else: - _LOGGER.debug("Discovered chromecast %s", info) + _LOGGER.debug("Discovered new or updated chromecast %s", info) - hass.data[KNOWN_CHROMECAST_INFO_KEY][info.uuid] = info dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) @@ -42,7 +49,7 @@ def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo): dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) -def setup_internal_discovery(hass: HomeAssistant) -> None: +def setup_internal_discovery(hass: HomeAssistant, config_entry) -> None: """Set up the pychromecast internal discovery.""" if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() @@ -51,72 +58,50 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: # Internal discovery is already running return - def internal_add_update_callback(uuid, service_name): - """Handle zeroconf discovery of a new or updated chromecast.""" - service = listener.services[uuid] + class CastListener(pychromecast.discovery.AbstractCastListener): + """Listener for discovering chromecasts.""" - # For support of deprecated IP based white listing - zconf = ChromeCastZeroconf.get_zeroconf() - service_info = None - tries = 0 - while service_info is None and tries < 4: - try: - service_info = zconf.get_service_info( - "_googlecast._tcp.local.", service_name - ) - except OSError: - # If the zeroconf fails to receive the necessary data we abort - # adding the service - break - tries += 1 + def add_cast(self, uuid, _): + """Handle zeroconf discovery of a new chromecast.""" + discover_chromecast(hass, browser.devices[uuid]) - if not service_info: - _LOGGER.warning( - "setup_internal_discovery failed to get info for %s, %s", - uuid, - service_name, + def update_cast(self, uuid, _): + """Handle zeroconf discovery of an updated chromecast.""" + discover_chromecast(hass, browser.devices[uuid]) + + def remove_cast(self, uuid, service, cast_info): + """Handle zeroconf discovery of a removed chromecast.""" + _remove_chromecast( + hass, + ChromecastInfo( + services=cast_info.services, + uuid=cast_info.uuid, + model_name=cast_info.model_name, + friendly_name=cast_info.friendly_name, + ), ) - return - - addresses = service_info.parsed_addresses() - host = addresses[0] if addresses else service_info.server - - discover_chromecast( - hass, - ChromecastInfo( - services=service[0], - uuid=service[1], - model_name=service[2], - friendly_name=service[3], - host=host, - port=service_info.port, - ), - ) - - def internal_remove_callback(uuid, service_name, service): - """Handle zeroconf discovery of a removed chromecast.""" - _remove_chromecast( - hass, - ChromecastInfo( - services=service[0], - uuid=service[1], - model_name=service[2], - friendly_name=service[3], - ), - ) _LOGGER.debug("Starting internal pychromecast discovery") - listener = pychromecast.CastListener( - internal_add_update_callback, - internal_remove_callback, - internal_add_update_callback, + browser = pychromecast.discovery.CastBrowser( + CastListener(), + ChromeCastZeroconf.get_zeroconf(), + config_entry.data.get(CONF_KNOWN_HOSTS), ) - browser = pychromecast.start_discovery(listener, ChromeCastZeroconf.get_zeroconf()) + hass.data[CAST_BROWSER_KEY] = browser + browser.start_discovery() def stop_discovery(event): """Stop discovery of new chromecasts.""" _LOGGER.debug("Stopping internal pychromecast discovery") - pychromecast.discovery.stop_discovery(browser) + browser.stop_discovery() hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) + + config_entry.add_update_listener(config_entry_updated) + + +async def config_entry_updated(hass, config_entry): + """Handle config entry being updated.""" + browser = hass.data[CAST_BROWSER_KEY] + browser.host_browser.update_hosts(config_entry.data.get(CONF_KNOWN_HOSTS)) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index b8742ec2b5e..71caa6490d8 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -7,8 +7,6 @@ import attr from pychromecast import dial from pychromecast.const import CAST_MANUFACTURERS -from .const import DEFAULT_PORT - @attr.s(slots=True, frozen=True) class ChromecastInfo: @@ -17,22 +15,16 @@ class ChromecastInfo: This also has the same attributes as the mDNS fields by zeroconf. """ - services: Optional[set] = attr.ib() - host: Optional[str] = attr.ib(default=None) - port: Optional[int] = attr.ib(default=0) - uuid: Optional[str] = attr.ib( + services: set | None = attr.ib() + uuid: str | None = attr.ib( converter=attr.converters.optional(str), default=None ) # always convert UUID to string if not None _manufacturer = attr.ib(type=Optional[str], default=None) model_name: str = attr.ib(default="") - friendly_name: Optional[str] = attr.ib(default=None) + friendly_name: str | None = attr.ib(default=None) + is_audio_group = attr.ib(type=Optional[bool], default=False) is_dynamic_group = attr.ib(type=Optional[bool], default=None) - @property - def is_audio_group(self) -> bool: - """Return if this is an audio group.""" - return self.port != DEFAULT_PORT - @property def is_information_complete(self) -> bool: """Return if all information is filled out.""" @@ -74,7 +66,7 @@ class ChromecastInfo: http_group_status = None if self.uuid: http_group_status = dial.get_multizone_status( - self.host, + None, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf(), ) @@ -86,17 +78,16 @@ class ChromecastInfo: return ChromecastInfo( services=self.services, - host=self.host, - port=self.port, uuid=self.uuid, friendly_name=self.friendly_name, model_name=self.model_name, + is_audio_group=True, is_dynamic_group=is_dynamic_group, ) # Fill out some missing information (friendly_name, uuid) via HTTP dial. http_device_status = dial.get_device_status( - self.host, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf() + None, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf() ) if http_device_status is None: # HTTP dial didn't give us any new information. @@ -104,8 +95,6 @@ class ChromecastInfo: return ChromecastInfo( services=self.services, - host=self.host, - port=self.port, uuid=(self.uuid or http_device_status.uuid), friendly_name=(self.friendly_name or http_device_status.friendly_name), manufacturer=(self.manufacturer or http_device_status.manufacturer), diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 3edc1ce2cde..bb0354bb68e 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -1,5 +1,5 @@ """Home Assistant Cast integration for Cast.""" -from typing import Optional +from __future__ import annotations from pychromecast.controllers.homeassistant import HomeAssistantController import voluptuous as vol @@ -20,8 +20,8 @@ async def async_setup_ha_cast( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up Home Assistant Cast.""" - user_id: Optional[str] = entry.data.get("user_id") - user: Optional[auth.models.User] = None + user_id: str | None = entry.data.get("user_id") + user: auth.models.User | None = None if user_id is not None: user = await hass.auth.async_get_user(user_id) @@ -78,7 +78,7 @@ async def async_remove_user( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Remove Home Assistant Cast user.""" - user_id: Optional[str] = entry.data.get("user_id") + user_id: str | None = entry.data.get("user_id") if user_id is not None: user = await hass.auth.async_get_user(user_id) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 28ccb78d5b9..3f30bc450fd 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==8.1.2"], + "requirements": ["pychromecast==9.1.2"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 981d67f0caa..b6ca8dd0728 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,10 +1,12 @@ """Provide functionality to interact with Cast devices on the network.""" +from __future__ import annotations + import asyncio +from contextlib import suppress from datetime import timedelta import functools as ft import json import logging -from typing import Optional import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -50,19 +52,19 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import callback -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.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 ConfigType, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro from .const import ( ADDED_CAST_DEVICES_KEY, CAST_MULTIZONE_MANAGER_KEY, + CONF_IGNORE_CEC, + CONF_UUID, DOMAIN as CAST_DOMAIN, - KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, @@ -72,8 +74,6 @@ from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) -CONF_IGNORE_CEC = "ignore_cec" -CONF_UUID = "uuid" CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" SUPPORT_CAST = ( @@ -127,43 +127,20 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Cast from a config entry.""" - config = hass.data[CAST_DOMAIN].get("media_player") or {} - if not isinstance(config, list): - config = [config] - - # no pending task - done, _ = await asyncio.wait( - [ - _async_setup_platform(hass, ENTITY_SCHEMA(cfg), async_add_entities) - for cfg in config - ] - ) - if any(task.exception() for task in done): - exceptions = [task.exception() for task in done] - for exception in exceptions: - _LOGGER.debug("Failed to setup chromecast", exc_info=exception) - raise PlatformNotReady - - -async def _async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities -): - """Set up the cast platform.""" - # Import CEC IGNORE attributes - pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) - hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, {}) - info = None - if CONF_UUID in config: - info = ChromecastInfo(uuid=config[CONF_UUID], services=None) + # Import CEC IGNORE attributes + pychromecast.IGNORE_CEC += config_entry.data.get(CONF_IGNORE_CEC) or [] + + wanted_uuids = config_entry.data.get(CONF_UUID) or None @callback def async_cast_discovered(discover: ChromecastInfo) -> None: """Handle discovery of a new chromecast.""" - # If info is set, we're handling a specific cast device identified by UUID - if info is not None and (info.uuid is not None and info.uuid != discover.uuid): - # UUID not matching, this is not it. + # If wanted_uuids is set, we're only accepting specific cast devices identified + # by UUID + if wanted_uuids is not None and discover.uuid not in wanted_uuids: + # UUID not matching, ignore. return cast_device = _async_create_cast_device(hass, discover) @@ -171,13 +148,8 @@ async def _async_setup_platform( async_add_entities([cast_device]) async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) - # Re-play the callback for all past chromecasts, store the objects in - # a list to avoid concurrent modification resulting in exception. - for chromecast in hass.data[KNOWN_CHROMECAST_INFO_KEY].values(): - async_cast_discovered(chromecast) - ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) - hass.async_add_executor_job(setup_internal_discovery, hass) + hass.async_add_executor_job(setup_internal_discovery, hass, config_entry) class CastDevice(MediaPlayerEntity): @@ -193,7 +165,7 @@ class CastDevice(MediaPlayerEntity): self._cast_info = cast_info self.services = cast_info.services - self._chromecast: Optional[pychromecast.Chromecast] = None + self._chromecast: pychromecast.Chromecast | None = None self.cast_status = None self.media_status = None self.media_status_received = None @@ -201,8 +173,8 @@ class CastDevice(MediaPlayerEntity): self.mz_media_status_received = {} self.mz_mgr = None self._available = False - self._status_listener: Optional[CastStatusListener] = None - self._hass_cast_controller: Optional[HomeAssistantController] = None + self._status_listener: CastStatusListener | None = None + self._hass_cast_controller: HomeAssistantController | None = None self._add_remove_handler = None self._cast_view_remove_handler = None @@ -214,7 +186,9 @@ class CastDevice(MediaPlayerEntity): ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.async_set_cast_info(self._cast_info) - self.hass.async_create_task( + # asyncio.create_task is used to avoid delaying startup wrapup if the device + # is discovered already during startup but then fails to respond + asyncio.create_task( async_create_catching_coro(self.async_connect_to_chromecast()) ) @@ -251,8 +225,8 @@ class CastDevice(MediaPlayerEntity): self.services, ) chromecast = await self.hass.async_add_executor_job( - pychromecast.get_chromecast_from_service, - ( + pychromecast.get_chromecast_from_cast_info, + pychromecast.discovery.CastInfo( self.services, self._cast_info.uuid, self._cast_info.model_name, @@ -327,21 +301,14 @@ class CastDevice(MediaPlayerEntity): tts_base_url = None url_description = "" if "tts" in self.hass.config.components: - try: + with suppress(KeyError): # base_url not configured tts_base_url = self.hass.components.tts.get_base_url(self.hass) - except KeyError: - # base_url not configured, ignore - pass - try: + + with suppress(NoURLAvailableError): # external_url not configured external_url = get_url(self.hass, allow_internal=False) - except NoURLAvailableError: - # external_url not configured, ignore - pass - try: + + with suppress(NoURLAvailableError): # internal_url not configured internal_url = get_url(self.hass, allow_external=False) - except NoURLAvailableError: - # internal_url not configured, ignore - pass if media_status.content_id: if tts_base_url and media_status.content_id.startswith(tts_base_url): @@ -742,15 +709,15 @@ class CastDevice(MediaPlayerEntity): support = SUPPORT_CAST media_status = self._media_status()[0] - if self.cast_status: - if self.cast_status.volume_control_type != VOLUME_CONTROL_TYPE_FIXED: - support |= SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + if ( + self.cast_status + and self.cast_status.volume_control_type != VOLUME_CONTROL_TYPE_FIXED + ): + support |= SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET if media_status: if media_status.supports_queue_next: - support |= SUPPORT_PREVIOUS_TRACK - if media_status.supports_queue_next: - support |= SUPPORT_NEXT_TRACK + support |= SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK if media_status.supports_seek: support |= SUPPORT_SEEK @@ -781,7 +748,7 @@ class CastDevice(MediaPlayerEntity): return media_status_recevied @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._cast_info.uuid @@ -803,7 +770,7 @@ class CastDevice(MediaPlayerEntity): controller: HomeAssistantController, entity_id: str, view_path: str, - url_path: Optional[str], + url_path: str | None, ): """Handle a show view signal.""" if entity_id != self.entity_id: @@ -825,9 +792,9 @@ class DynamicCastGroup: self.hass = hass self._cast_info = cast_info self.services = cast_info.services - self._chromecast: Optional[pychromecast.Chromecast] = None + self._chromecast: pychromecast.Chromecast | None = None self.mz_mgr = None - self._status_listener: Optional[CastStatusListener] = None + self._status_listener: CastStatusListener | None = None self._add_remove_handler = None self._del_remove_handler = None @@ -875,8 +842,8 @@ class DynamicCastGroup: self.services, ) chromecast = await self.hass.async_add_executor_job( - pychromecast.get_chromecast_from_service, - ( + pychromecast.get_chromecast_from_cast_info, + pychromecast.discovery.CastInfo( self.services, self._cast_info.uuid, self._cast_info.model_name, diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index ad8f0f41ae7..33ce4b6941e 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -3,11 +3,35 @@ "step": { "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" + }, + "config": { + "title": "Google Cast", + "description": "Please enter the Google Cast configuration.", + "data": { + "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + } } }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + } + }, + "options": { + "step": { + "options": { + "description": "Please enter the Google Cast configuration.", + "data": { + "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.", + "known_hosts": "Optional list of known hosts if mDNS discovery is not working.", + "uuid": "Optional list of UUIDs. Casts not listed will not be added." + } + } + }, + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." } } } diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json index dc21c371e60..6a5d16aa6bb 100644 --- a/homeassistant/components/cast/translations/ca.json +++ b/homeassistant/components/cast/translations/ca.json @@ -4,10 +4,35 @@ "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, + "error": { + "invalid_known_hosts": "Els amfitrions coneguts han de ser una llista d'amfitrions separats per comes." + }, "step": { + "config": { + "data": { + "known_hosts": "Llista opcional d'amfitrions coneguts per si el descobriment mDNS deixa de funcionar." + }, + "description": "Introdueix la configuraci\u00f3 de Google Cast.", + "title": "Google Cast" + }, "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Els amfitrions coneguts han de ser una llista d'amfitrions separats per comes." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Llista opcional que es passar\u00e0 a pychromecast.IGNORE_CEC.", + "known_hosts": "Llista opcional d'amfitrions coneguts per si el descobriment mDNS deixa de funcionar.", + "uuid": "Llista opcional d'UUIDs. No s'afegiran 'casts' que no siguin a la llista." + }, + "description": "Introdueix la configuraci\u00f3 de Google Cast." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 7ff1efb8ee0..a59b3421c10 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -4,10 +4,33 @@ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden.", "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." }, + "error": { + "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein." + }, "step": { + "config": { + "data": { + "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert." + }, + "description": "Bitte die Google Cast-Konfiguration eingeben.", + "title": "Google Cast" + }, "confirm": { "description": "M\u00f6chtest du Google Cast einrichten?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein." + }, + "step": { + "options": { + "data": { + "known_hosts": "Optionale Liste bekannter Hosts, wenn die mDNS-Erkennung nicht funktioniert." + }, + "description": "Bitte die Google Cast-Konfiguration eingeben." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index f05becffed3..c2c2460cc9c 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -4,10 +4,35 @@ "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + }, "step": { + "config": { + "data": { + "known_hosts": "Optional list of known hosts if mDNS discovery is not working." + }, + "description": "Please enter the Google Cast configuration.", + "title": "Google Cast" + }, "confirm": { "description": "Do you want to start set up?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Optional list which will be passed to pychromecast.IGNORE_CEC.", + "known_hosts": "Optional list of known hosts if mDNS discovery is not working.", + "uuid": "Optional list of UUIDs. Casts not listed will not be added." + }, + "description": "Please enter the Google Cast configuration." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/es-419.json b/homeassistant/components/cast/translations/es-419.json index c62ece17721..ee30ef16b46 100644 --- a/homeassistant/components/cast/translations/es-419.json +++ b/homeassistant/components/cast/translations/es-419.json @@ -9,5 +9,15 @@ "description": "\u00bfDesea configurar Google Cast?" } } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_cec": "Lista opcional que se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "uuid": "Lista opcional de UUID. No se agregar\u00e1n los dispositivos Cast que no est\u00e9n listados." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 520df7ee4cd..07b090634e0 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -4,10 +4,33 @@ "no_devices_found": "No se encontraron dispositivos en la red", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, + "error": { + "invalid_known_hosts": "Los hosts conocidos deben ser una lista de hosts separados por comas." + }, "step": { + "config": { + "data": { + "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." + }, + "description": "Introduce la configuraci\u00f3n de Google Cast.", + "title": "Google Cast" + }, "confirm": { "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Los hosts conocidos deben ser una lista de hosts separados por comas." + }, + "step": { + "options": { + "data": { + "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." + }, + "description": "Introduce la configuraci\u00f3n de Google Cast." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/et.json b/homeassistant/components/cast/translations/et.json index 05287b5a52b..6397951272a 100644 --- a/homeassistant/components/cast/translations/et.json +++ b/homeassistant/components/cast/translations/et.json @@ -4,10 +4,35 @@ "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi Google Casti seadet.", "single_instance_allowed": "Vajalik on ainult \u00fcks Google Casti konfiguratsioon." }, + "error": { + "invalid_known_hosts": "Teadaolevad hostid peab olema komaeraldusega hostide loend." + }, "step": { + "config": { + "data": { + "known_hosts": "Valikuline loend teadaolevatest hostidest kui mDNS-i tuvastamine ei t\u00f6\u00f6ta." + }, + "description": "Sisesta Google Casti andmed.", + "title": "Google Cast" + }, "confirm": { "description": "Kas soovid seadistada Google Casti?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Teadaolevad hostid peab olema komaeraldusega hostide loend." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Valikuline nimekiri mis edastatakse pychromecast.IGNORE_CEC-ile.", + "known_hosts": "Valikuline loend teadaolevatest hostidest kui mDNS-i tuvastamine ei t\u00f6\u00f6ta.", + "uuid": "Valikuline UUIDide loend. Loetlemata cast-e ei lisata." + }, + "description": "Sisesta Google Casti andmed." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index afa4b094fad..0acfd327e3e 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -4,10 +4,33 @@ "no_devices_found": "Aucun appareil Google Cast trouv\u00e9 sur le r\u00e9seau.", "single_instance_allowed": "Une seule configuration de Google Cast est n\u00e9cessaire." }, + "error": { + "invalid_known_hosts": "Les h\u00f4tes connus doivent \u00eatre une liste d'h\u00f4tes s\u00e9par\u00e9s par des virgules." + }, "step": { + "config": { + "data": { + "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas." + }, + "description": "Veuillez saisir la configuration de Google Cast.", + "title": "Google Cast" + }, "confirm": { "description": "Voulez-vous configurer Google Cast?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Les h\u00f4tes connus doivent \u00eatre une liste d'h\u00f4tes s\u00e9par\u00e9s par des virgules." + }, + "step": { + "options": { + "data": { + "known_hosts": "Liste facultative des h\u00f4tes connus si la d\u00e9couverte mDNS ne fonctionne pas." + }, + "description": "Veuillez saisir la configuration de Google Cast." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index dc55cd224f8..4a6ef76f33c 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -1,12 +1,35 @@ { "config": { "abort": { - "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", - "single_instance_allowed": "Csak egyetlen Google Cast konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_known_hosts": "Az ismert hosztoknak vessz\u0151vel elv\u00e1lasztott hosztok list\u00e1j\u00e1nak kell lennie." }, "step": { + "config": { + "data": { + "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik." + }, + "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t.", + "title": "Google Cast" + }, "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?" + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "options": { + "error": { + "invalid_known_hosts": "Az ismert hosztoknak vessz\u0151vel elv\u00e1lasztott hosztok list\u00e1j\u00e1nak kell lennie." + }, + "step": { + "options": { + "data": { + "known_hosts": "Opcion\u00e1lis lista az ismert hosztokr\u00f3l, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik." + }, + "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 d3e2bb5f360..240ee853609 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -1,12 +1,35 @@ { "config": { "abort": { - "no_devices_found": "Tidak ada perangkat Google Cast yang ditemukan pada jaringan.", - "single_instance_allowed": "Hanya satu konfigurasi Google Cast yang diperlukan." + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_known_hosts": "Host yang diketahui harus berupa daftar host yang dipisahkan koma." }, "step": { + "config": { + "data": { + "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi." + }, + "description": "Masukkan konfigurasi Google Cast.", + "title": "Google Cast" + }, "confirm": { - "description": "Apakah Anda ingin menyiapkan Google Cast?" + "description": "Ingin memulai penyiapan?" + } + } + }, + "options": { + "error": { + "invalid_known_hosts": "Host yang diketahui harus berupa daftar host yang dipisahkan koma." + }, + "step": { + "options": { + "data": { + "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi." + }, + "description": "Masukkan konfigurasi Google Cast." } } } diff --git a/homeassistant/components/cast/translations/it.json b/homeassistant/components/cast/translations/it.json index 0278fe07bfe..83586bf9f2c 100644 --- a/homeassistant/components/cast/translations/it.json +++ b/homeassistant/components/cast/translations/it.json @@ -4,10 +4,35 @@ "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "error": { + "invalid_known_hosts": "Gli host noti devono essere un elenco di host separato da virgole." + }, "step": { + "config": { + "data": { + "known_hosts": "Elenco facoltativo di host noti se l'individuazione di mDNS non funziona." + }, + "description": "Inserisci la configurazione di Google Cast.", + "title": "Google Cast" + }, "confirm": { "description": "Vuoi iniziare la configurazione?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Gli host noti devono essere indicati sotto forma di un elenco di host separati da virgole." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Elenco opzionale che sar\u00e0 passato a pychromecast.IGNORE_CEC.", + "known_hosts": "Elenco facoltativo di host noti se l'individuazione di mDNS non funziona.", + "uuid": "Elenco opzionale di UUID. I cast non elencati non saranno aggiunti." + }, + "description": "Inserisci la configurazione di Google Cast." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ko.json b/homeassistant/components/cast/translations/ko.json index 7011a61f757..b0bfd3271c9 100644 --- a/homeassistant/components/cast/translations/ko.json +++ b/homeassistant/components/cast/translations/ko.json @@ -2,12 +2,37 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." + }, + "error": { + "invalid_known_hosts": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ud638\uc2a4\ud2b8 \ubaa9\ub85d\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "step": { + "config": { + "data": { + "known_hosts": "mDNS \uac80\uc0c9\uc774 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0 \uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4." + }, + "description": "Google Cast \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Google Cast" + }, "confirm": { "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ud638\uc2a4\ud2b8 \ubaa9\ub85d\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, + "step": { + "options": { + "data": { + "ignore_cec": "pychromecast.IGNORE_CEC\uc5d0 \uc804\ub2ec\ub420 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4.", + "known_hosts": "mDNS \uac80\uc0c9\uc774 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0 \uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4.", + "uuid": "UUID\uc758 \uc120\ud0dd\uc801 \ubaa9\ub85d\uc785\ub2c8\ub2e4. \ubaa9\ub85d\uc5d0 \uc5c6\ub294 \uce90\uc2a4\ud2b8\ub294 \ucd94\uac00\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "description": "Google Cast \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index d42ef3e850c..02bf7514761 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -1,12 +1,37 @@ { "config": { "abort": { - "no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.", - "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "invalid_known_hosts": "Bekende hosts moet een door komma's gescheiden lijst van hosts zijn." }, "step": { + "config": { + "data": { + "known_hosts": "Optionele lijst van bekende hosts indien mDNS discovery niet werkt." + }, + "description": "Voer de Google Cast configuratie in.", + "title": "Google Cast" + }, "confirm": { - "description": "Wilt u Google Cast instellen?" + "description": "Wil je beginnen met instellen?" + } + } + }, + "options": { + "error": { + "invalid_known_hosts": "Bekende hosts moet een door komma's gescheiden lijst van hosts zijn." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Optionele lijst die zal worden doorgegeven aan pychromecast.IGNORE_CEC.", + "known_hosts": "Optionele lijst van bekende hosts indien mDNS discovery niet werkt.", + "uuid": "Optionele lijst van UUID's. Casts die niet in de lijst staan, worden niet toegevoegd." + }, + "description": "Voer de Google Cast configuratie in." } } } diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json index b3d6b5d782e..0c9b3d93dce 100644 --- a/homeassistant/components/cast/translations/no.json +++ b/homeassistant/components/cast/translations/no.json @@ -4,10 +4,35 @@ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, + "error": { + "invalid_known_hosts": "Kjente verter m\u00e5 v\u00e6re en kommaseparert liste over verter." + }, "step": { + "config": { + "data": { + "known_hosts": "Valgfri liste over kjente verter hvis mDNS-oppdagelse ikke fungerer." + }, + "description": "Angi Google Cast-konfigurasjonen.", + "title": "Google Cast" + }, "confirm": { "description": "Vil du starte oppsettet?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Kjente verter m\u00e5 v\u00e6re en kommaseparert liste over verter." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Valgfri liste som sendes til pychromecast.IGNORE_CEC.", + "known_hosts": "Valgfri liste over kjente verter hvis mDNS-oppdagelse ikke fungerer.", + "uuid": "Valgfri liste over UUIDer. Medvirkende som ikke er oppf\u00f8rt, blir ikke lagt til." + }, + "description": "Angi Google Cast-konfigurasjonen." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/pl.json b/homeassistant/components/cast/translations/pl.json index a8ee3fa57ac..5802bda502a 100644 --- a/homeassistant/components/cast/translations/pl.json +++ b/homeassistant/components/cast/translations/pl.json @@ -4,10 +4,35 @@ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, + "error": { + "invalid_known_hosts": "Znane hosty musz\u0105 by\u0107 list\u0105 host\u00f3w oddzielonych przecinkami." + }, "step": { + "config": { + "data": { + "known_hosts": "Opcjonalna lista znanych host\u00f3w, je\u015bli wykrywanie mDNS nie dzia\u0142a." + }, + "description": "Wprowad\u017a konfiguracj\u0119 Google Cast.", + "title": "Google Cast" + }, "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Znane hosty musz\u0105 by\u0107 list\u0105 host\u00f3w oddzielonych przecinkami." + }, + "step": { + "options": { + "data": { + "ignore_cec": "Opcjonalna lista, kt\u00f3ra zostanie przekazana do pychromecast.IGNORE_CEC.", + "known_hosts": "Opcjonalna lista znanych host\u00f3w, je\u015bli wykrywanie mDNS nie dzia\u0142a.", + "uuid": "Opcjonalna lista identyfikator\u00f3w UUID. Casty nie wymienione na li\u015bcie nie zostan\u0105 dodane." + }, + "description": "Wprowad\u017a konfiguracj\u0119 Google Cast." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index 9dd9a69a94c..2a5b62a9de1 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -5,9 +5,19 @@ "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." }, "step": { + "config": { + "title": "Google Cast" + }, "confirm": { "description": "Deseja configurar o Google Cast?" } } + }, + "options": { + "step": { + "options": { + "description": "Por favor introduza a configura\u00e7\u00e3o do Google Cast" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/ru.json b/homeassistant/components/cast/translations/ru.json index 85a42bf1be5..7c412476151 100644 --- a/homeassistant/components/cast/translations/ru.json +++ b/homeassistant/components/cast/translations/ru.json @@ -4,10 +4,35 @@ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, + "error": { + "invalid_known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u0441\u043f\u0438\u0441\u043a\u043e\u043c, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438." + }, "step": { + "config": { + "data": { + "known_hosts": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0445\u043e\u0441\u0442\u043e\u0432, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442." + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Cast.", + "title": "Google Cast" + }, "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "\u0418\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0435 \u0445\u043e\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u0441\u043f\u0438\u0441\u043a\u043e\u043c, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438." + }, + "step": { + "options": { + "data": { + "ignore_cec": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d \u0432 pychromecast.IGNORE_CEC.", + "known_hosts": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0445\u043e\u0441\u0442\u043e\u0432, \u0435\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 mDNS \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", + "uuid": "\u041d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a UUID. \u041d\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u043d\u044b\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0435 \u0431\u0443\u0434\u0443\u0442." + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Google Cast." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/zh-Hans.json b/homeassistant/components/cast/translations/zh-Hans.json index 1c2024f8b81..0feaac56440 100644 --- a/homeassistant/components/cast/translations/zh-Hans.json +++ b/homeassistant/components/cast/translations/zh-Hans.json @@ -5,9 +5,20 @@ "single_instance_allowed": "Google Cast \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" }, "step": { + "config": { + "description": "\u8bf7\u786e\u8ba4Goole Cast\u7684\u914d\u7f6e", + "title": "Google Cast" + }, "confirm": { "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f" } } + }, + "options": { + "step": { + "options": { + "description": "\u8bf7\u786e\u8ba4Goole Cast\u7684\u914d\u7f6e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 90c98e491df..00810ade520 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -4,10 +4,35 @@ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, + "error": { + "invalid_known_hosts": "\u5df2\u77e5\u4e3b\u6a5f\u5fc5\u9808\u4ee5\u9017\u865f\u5206\u4e3b\u6a5f\u5217\u8868\u3002" + }, "step": { + "config": { + "data": { + "known_hosts": "\u5047\u5982 mDNS \u63a2\u7d22\u7121\u6cd5\u4f5c\u7528\uff0c\u5247\u70ba\u5df2\u77e5\u4e3b\u6a5f\u7684\u9078\u9805\u5217\u8868\u3002" + }, + "description": "\u8acb\u8f38\u5165 Google Cast \u8a2d\u5b9a\u3002", + "title": "Google Cast" + }, "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" } } + }, + "options": { + "error": { + "invalid_known_hosts": "\u5df2\u77e5\u4e3b\u6a5f\u5fc5\u9808\u4ee5\u9017\u865f\u5206\u4e3b\u6a5f\u5217\u8868\u3002" + }, + "step": { + "options": { + "data": { + "ignore_cec": "\u9078\u9805\u5217\u8868\u5c07\u50b3\u905e\u81f3 pychromecast.IGNORE_CEC\u3002", + "known_hosts": "\u5047\u5982 mDNS \u63a2\u7d22\u7121\u6cd5\u4f5c\u7528\uff0c\u5247\u70ba\u5df2\u77e5\u4e3b\u6a5f\u7684\u9078\u9805\u5217\u8868\u3002", + "uuid": "UUID \u9078\u9805\u5217\u8868\u3002\u672a\u5217\u51fa\u7684 Cast \u88dd\u7f6e\u5c07\u4e0d\u6703\u9032\u884c\u65b0\u589e\u3002" + }, + "description": "\u8acb\u8f38\u5165 Google Cast \u8a2d\u5b9a\u3002" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index d01e38a2e2c..22b3ce56129 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1,11 +1,11 @@ """The cert_expiry component.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging -from typing import Optional from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,21 +18,13 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=12) -async def async_setup(hass, config): - """Platform setup, do nothing.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Load the saved entities.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] coordinator = CertExpiryDataUpdateCoordinator(hass, host, port) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator @@ -71,7 +63,7 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime]): update_interval=SCAN_INTERVAL, ) - async def _async_update_data(self) -> Optional[datetime]: + async def _async_update_data(self) -> datetime | None: """Fetch certificate.""" try: timestamp = await get_cert_expiry_timestamp(self.hass, self.host, self.port) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 282c87b25c5..bfa5f46190b 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT -from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import +from .const import DEFAULT_PORT, DOMAIN from .errors import ( ConnectionRefused, ConnectionTimeout, diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index 6c49e9e26b9..c00a99c8e86 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -19,8 +19,7 @@ def get_cert(host, port): address = (host, port) with socket.create_connection(address, timeout=TIMEOUT) as sock: with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: - # pylint disable: https://github.com/PyCQA/pylint/issues/3166 - cert = ssock.getpeercert() # pylint: disable=no-member + cert = ssock.getpeercert() return cert diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 0e329b1898f..a05acdb5d77 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, @@ -68,7 +68,7 @@ class CertExpiryEntity(CoordinatorEntity): return "mdi:certificate" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return additional sensor state attributes.""" return { "is_valid": self.coordinator.is_cert_valid, @@ -76,7 +76,7 @@ class CertExpiryEntity(CoordinatorEntity): } -class SSLCertificateTimestamp(CertExpiryEntity): +class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): """Implementation of the Cert Expiry timestamp sensor.""" @property diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json index 5bad24ecb6a..2ae516565e3 100644 --- a/homeassistant/components/cert_expiry/translations/hu.json +++ b/homeassistant/components/cert_expiry/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "import_failed": "Nem siker\u00fclt import\u00e1lni a konfigur\u00e1ci\u00f3t" }, "step": { diff --git a/homeassistant/components/cert_expiry/translations/id.json b/homeassistant/components/cert_expiry/translations/id.json new file mode 100644 index 00000000000..9fac285fe82 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "import_failed": "Impor dari konfigurasi gagal" + }, + "error": { + "connection_refused": "Sambungan ditolak saat menghubungkan ke host", + "connection_timeout": "Tenggang waktu terhubung ke host ini habis", + "resolve_failed": "Host ini tidak dapat ditemukan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama sertifikat", + "port": "Port" + }, + "title": "Tentukan sertifikat yang akan diuji" + } + } + }, + "title": "Informasi Kedaluwarsa Sertifikat" +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/nl.json b/homeassistant/components/cert_expiry/translations/nl.json index d844d28e62f..e3cd3d7983b 100644 --- a/homeassistant/components/cert_expiry/translations/nl.json +++ b/homeassistant/components/cert_expiry/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze combinatie van host en poort is al geconfigureerd", + "already_configured": "Service is al geconfigureerd", "import_failed": "Importeren vanuit configuratie is mislukt" }, "error": { @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "De hostnaam van het certificaat", + "host": "Host", "name": "De naam van het certificaat", - "port": "De poort van het certificaat" + "port": "Poort" }, "title": "Het certificaat defini\u00ebren dat moet worden getest" } diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index 8bf2b77fa25..0c77fc6fd7e 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -47,7 +47,7 @@ class CiscoDeviceScanner(DeviceScanner): self.last_results = {} self.success_init = self._update_info() - _LOGGER.info("cisco_ios scanner initialized") + _LOGGER.info("Initialized cisco_ios scanner") def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device.""" @@ -131,8 +131,7 @@ class CiscoDeviceScanner(DeviceScanner): return devices_result.decode("utf-8") except pxssh.ExceptionPxssh as px_e: - _LOGGER.error("pxssh failed on login") - _LOGGER.error(px_e) + _LOGGER.error("Failed to login via pxssh: %s", px_e) return None diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 924ef2fa6b4..bc323a51151 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -7,7 +7,11 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ID, @@ -25,7 +29,7 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import distance, location @@ -258,7 +262,7 @@ class CityBikesNetwork: raise PlatformNotReady from err -class CityBikesStation(Entity): +class CityBikesStation(SensorEntity): """CityBikes API Sensor.""" def __init__(self, network, station_id, entity_id): @@ -286,7 +290,7 @@ class CityBikesStation(Entity): break @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._station_data: return { diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index b6e70ab56e8..1498c51f54a 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -1,9 +1,11 @@ """The ClimaCell integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging from math import ceil -from typing import Any, Dict, Optional, Union +from typing import Any from pyclimacell import ClimaCell from pyclimacell.const import ( @@ -22,9 +24,8 @@ from pyclimacell.pyclimacell import ( 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.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -80,11 +81,6 @@ def _set_update_interval( return interval -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the ClimaCell API component.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up ClimaCell API from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -110,16 +106,13 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) _set_update_interval(hass, config_entry), ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][config_entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -132,8 +125,8 @@ async def async_unload_entry( unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -169,7 +162,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): update_interval=update_interval, ) - async def _async_update_data(self) -> Dict[str, Any]: + async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" data = {FORECASTS: {}} try: @@ -217,8 +210,8 @@ class ClimaCellEntity(CoordinatorEntity): @staticmethod def _get_cc_value( - weather_dict: Dict[str, Any], key: str - ) -> Optional[Union[int, float, str]]: + weather_dict: dict[str, Any], key: str + ) -> int | float | str | None: """Return property from weather_dict.""" items = weather_dict.get(key, {}) # Handle cases where value returned is a list. @@ -252,7 +245,7 @@ class ClimaCellEntity(CoordinatorEntity): return ATTRIBUTION @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index 09e02f3f559..ebf63abcae4 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -1,6 +1,8 @@ """Config flow for ClimaCell integration.""" +from __future__ import annotations + import logging -from typing import Any, Dict +from typing import Any from pyclimacell import ClimaCell from pyclimacell.const import REALTIME @@ -18,14 +20,13 @@ 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 -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_TIMESTEP, DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN _LOGGER = logging.getLogger(__name__) def _get_config_schema( - hass: core.HomeAssistant, input_dict: Dict[str, Any] = None + hass: core.HomeAssistant, input_dict: dict[str, Any] = None ) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. @@ -57,7 +58,7 @@ def _get_config_schema( ) -def _get_unique_id(hass: HomeAssistantType, input_dict: Dict[str, Any]): +def _get_unique_id(hass: HomeAssistantType, input_dict: dict[str, Any]): """Return unique ID from config data.""" return ( f"{input_dict[CONF_API_KEY]}" @@ -74,8 +75,8 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): self._config_entry = config_entry async def async_step_init( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """Manage the ClimaCell options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -107,10 +108,9 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return ClimaCellOptionsConfigFlow(config_entry) async def async_step_user( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """Handle the initial step.""" - assert self.hass errors = {} if user_input is not None: await self.async_set_unique_id( diff --git a/homeassistant/components/climacell/translations/bg.json b/homeassistant/components/climacell/translations/bg.json new file mode 100644 index 00000000000..6b1e4d3cba2 --- /dev/null +++ b/homeassistant/components/climacell/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index f18197e1cca..7ec41d01733 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "rate_limited": "Aktuelle Aktualisierungsrate gedrosselt, bitte versuche es sp\u00e4ter erneut.", "unknown": "Unerwarteter Fehler" }, "step": { @@ -12,8 +13,22 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" - } + }, + "description": "Wenn Breitengrad und L\u00e4ngengrad nicht angegeben werden, werden die Standardwerte in der Home Assistant-Konfiguration verwendet. F\u00fcr jeden Vorhersagetyp wird eine Entit\u00e4t erstellt, aber nur die von Ihnen ausgew\u00e4hlten werden standardm\u00e4\u00dfig aktiviert." } } - } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Vorhersage Arten", + "timestep": "Minuten zwischen den Kurzvorhersagen" + }, + "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", + "title": "Aktualisiere ClimaCell-Optionen" + } + } + }, + "title": "ClimaCell" } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json new file mode 100644 index 00000000000..45ed5d8a722 --- /dev/null +++ b/homeassistant/components/climacell/translations/el.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03a0\u03bb\u03ac\u03c4\u03bf\u03c2", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u039c\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2, \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Home Assistant. \u0398\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5, \u03b1\u03bb\u03bb\u03ac \u03bc\u03cc\u03bd\u03bf \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03b5\u03c4\u03b5 \u03b8\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "\u0395\u03af\u03b4\u03bf\u03c2/\u03b7 \u0394\u03b5\u03bb\u03c4\u03af\u03bf\u03c5/\u03c9\u03bd", + "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 4c4d8fcc9bb..52fd5d21166 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -1,7 +1,9 @@ { "config": { "error": { - "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde." + "cannot_connect": "Fallo al conectar", + "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", + "unknown": "Error inesperado" }, "step": { "user": { diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 8fd3f7b7122..3b3aa3d18ba 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_api_key": "Cl\u00e9 API invalide", - "rate_limited": "Currently rate limited, please try again later.", + "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json new file mode 100644 index 00000000000..fa0aa2ec0c7 --- /dev/null +++ b/homeassistant/components/climacell/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Ha a Sz\u00e9less\u00e9g \u00e9s Hossz\u00fas\u00e1g nincs megadva, akkor a Home Assistant konfigur\u00e1ci\u00f3j\u00e1ban l\u00e9v\u0151 alap\u00e9rtelmezett \u00e9rt\u00e9keket fogjuk haszn\u00e1lni. Minden el\u0151rejelz\u00e9si t\u00edpushoz l\u00e9trej\u00f6n egy entit\u00e1s, de alap\u00e9rtelmez\u00e9s szerint csak az \u00e1ltalad kiv\u00e1lasztottak lesznek enged\u00e9lyezve." + } + } + }, + "options": { + "step": { + "init": { + "title": "Friss\u00edtse a ClimaCell be\u00e1ll\u00edt\u00e1sokat" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json new file mode 100644 index 00000000000..132f4dcfcb7 --- /dev/null +++ b/homeassistant/components/climacell/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "rate_limited": "Saat ini tingkatnya dibatasi, coba lagi nanti.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Jika Lintang dan Bujur tidak tersedia, nilai default dalam konfigurasi Home Assistant akan digunakan. Entitas akan dibuat untuk setiap jenis prakiraan tetapi hanya yang Anda pilih yang akan diaktifkan secara default." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Jenis Prakiraan", + "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" + }, + "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan 'nowcast', Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.", + "title": "Perbarui Opsi ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json new file mode 100644 index 00000000000..6fc5a6d7e8b --- /dev/null +++ b/homeassistant/components/climacell/translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "rate_limited": "\ud604\uc7ac \uc0ac\uc6a9 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "description": "\uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 Home Assistant \uad6c\uc131\uc758 \uae30\ubcf8\uac12\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \uac01 \uc77c\uae30\uc608\ubcf4 \uc720\ud615\uc5d0 \ub300\ud574 \uad6c\uc131\uc694\uc18c\uac00 \uc0dd\uc131\ub418\uc9c0\ub9cc \uae30\ubcf8\uc801\uc73c\ub85c \uc120\ud0dd\ud55c \uad6c\uc131\uc694\uc18c\ub9cc \ud65c\uc131\ud654\ub429\ub2c8\ub2e4." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "\uc77c\uae30\uc608\ubcf4 \uc720\ud615", + "timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04" + }, + "description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.", + "title": "ClimaCell \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json index 488a43ae24e..f267be34478 100644 --- a/homeassistant/components/climacell/translations/nl.json +++ b/homeassistant/components/climacell/translations/nl.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_api_key": "Ongeldige API-sleutel", + "rate_limited": "Momenteel is een beperkt aantal aanvragen mogelijk, probeer later opnieuw.", "unknown": "Onverwachte fout" }, "step": { @@ -13,7 +14,7 @@ "longitude": "Lengtegraad", "name": "Naam" }, - "description": "Indien Breedtegraad en Lengtegraad niet worden opgegeven, worden de standaardwaarden in de Home Assistant-configuratie gebruikt. Er wordt een entiteit gemaakt voor elk voorspellingstype maar alleen degene die u selecteert worden standaard ingeschakeld." + "description": "Indien Breedtegraad en Lengtegraad niet worden opgegeven, worden de standaardwaarden in de Home Assistant-configuratie gebruikt. Er wordt een entiteit gemaakt voor elk voorspellingstype, maar alleen degenen die u selecteert worden standaard ingeschakeld." } } }, @@ -21,7 +22,8 @@ "step": { "init": { "data": { - "forecast_types": "Voorspellingstype(n)" + "forecast_types": "Voorspellingstype(n)", + "timestep": "Min. Tussen NowCast-voorspellingen" }, "description": "Als u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", "title": "Update ClimaCell Opties" diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json new file mode 100644 index 00000000000..6fc13aadc96 --- /dev/null +++ b/homeassistant/components/climacell/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "rate_limited": "Przekroczono limit, spr\u00f3buj ponownie p\u00f3\u017aniej.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Je\u015bli szeroko\u015b\u0107 i d\u0142ugo\u015b\u0107 geograficzna nie zostan\u0105 podane, zostan\u0105 u\u017cyte domy\u015blne warto\u015bci z konfiguracji Home Assistanta. Zostanie utworzona encja dla ka\u017cdego typu prognozy, ale domy\u015blnie w\u0142\u0105czone bed\u0105 tylko te, kt\u00f3re wybierzesz." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast_types": "Typ(y) prognozy", + "timestep": "Minuty pomi\u0119dzy prognozami" + }, + "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.", + "title": "Opcje aktualizacji ClimaCell" + } + } + }, + "title": "ClimaCell" +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pt.json b/homeassistant/components/climacell/translations/pt.json new file mode 100644 index 00000000000..8e05df2f1b5 --- /dev/null +++ b/homeassistant/components/climacell/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hans.json b/homeassistant/components/climacell/translations/zh-Hans.json new file mode 100644 index 00000000000..315d060bc69 --- /dev/null +++ b/homeassistant/components/climacell/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key", + "name": "\u540d\u5b57" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index e5a24197d6b..b9da5431dd0 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -1,7 +1,9 @@ """Weather component that handles meteorological data for your location.""" +from __future__ import annotations + from datetime import datetime import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -64,9 +66,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _translate_condition( - condition: Optional[str], sun_is_up: bool = True -) -> Optional[str]: +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 @@ -82,13 +82,13 @@ def _forecast_dict( forecast_dt: datetime, use_datetime: bool, condition: str, - precipitation: Optional[float], - precipitation_probability: Optional[float], - temp: Optional[float], - temp_low: Optional[float], - wind_direction: Optional[float], - wind_speed: Optional[float], -) -> Dict[str, Any]: + 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)) @@ -120,7 +120,7 @@ def _forecast_dict( async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] @@ -274,13 +274,12 @@ class ClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): ), temp_low, ) - elif self.forecast_type == NOWCAST: + elif self.forecast_type == NOWCAST and precipitation: # 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] - ) + precipitation = ( + precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] + ) forecasts.append( _forecast_dict( diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 32dfaa0e8fb..30842f1fe23 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,9 +1,11 @@ """Provides functionality to interact with climate devices.""" +from __future__ import annotations + from abc import abstractmethod from datetime import timedelta import functools as ft import logging -from typing import Any, Dict, List, Optional +from typing import Any, final import voluptuous as vol @@ -17,6 +19,7 @@ from homeassistant.const import ( STATE_ON, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -26,7 +29,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.util.temperature import convert as convert_temperature from .const import ( @@ -100,7 +103,7 @@ SET_TEMPERATURE_SCHEMA = vol.All( ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up climate entities.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -154,18 +157,18 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry): +async def async_setup_entry(hass: HomeAssistant, entry): """Set up a config entry.""" return await hass.data[DOMAIN].async_setup_entry(entry) -async def async_unload_entry(hass: HomeAssistantType, entry): +async def async_unload_entry(hass: HomeAssistant, entry): """Unload a config entry.""" return await hass.data[DOMAIN].async_unload_entry(entry) class ClimateEntity(Entity): - """Representation of a climate entity.""" + """Base class for climate entities.""" @property def state(self) -> str: @@ -180,7 +183,7 @@ class ClimateEntity(Entity): return PRECISION_WHOLE @property - def capability_attributes(self) -> Optional[Dict[str, Any]]: + def capability_attributes(self) -> dict[str, Any] | None: """Return the capability attributes.""" supported_features = self.supported_features data = { @@ -211,8 +214,9 @@ class ClimateEntity(Entity): return data + @final @property - def state_attributes(self) -> Dict[str, Any]: + def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" supported_features = self.supported_features data = { @@ -275,12 +279,12 @@ class ClimateEntity(Entity): raise NotImplementedError() @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> int | None: """Return the current humidity.""" return None @property - def target_humidity(self) -> Optional[int]: + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return None @@ -294,14 +298,14 @@ class ClimateEntity(Entity): @property @abstractmethod - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. """ @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported. Need to be one of CURRENT_HVAC_*. @@ -309,22 +313,22 @@ class ClimateEntity(Entity): return None @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return None @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return None @property - def target_temperature_step(self) -> Optional[float]: + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" return None @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach. Requires SUPPORT_TARGET_TEMPERATURE_RANGE. @@ -332,7 +336,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach. Requires SUPPORT_TARGET_TEMPERATURE_RANGE. @@ -340,7 +344,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. Requires SUPPORT_PRESET_MODE. @@ -348,7 +352,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. Requires SUPPORT_PRESET_MODE. @@ -356,7 +360,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def is_aux_heat(self) -> Optional[bool]: + def is_aux_heat(self) -> bool | None: """Return true if aux heater. Requires SUPPORT_AUX_HEAT. @@ -364,7 +368,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting. Requires SUPPORT_FAN_MODE. @@ -372,7 +376,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return the list of available fan modes. Requires SUPPORT_FAN_MODE. @@ -380,7 +384,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def swing_mode(self) -> Optional[str]: + def swing_mode(self) -> str | None: """Return the swing setting. Requires SUPPORT_SWING_MODE. @@ -388,7 +392,7 @@ class ClimateEntity(Entity): raise NotImplementedError @property - def swing_modes(self) -> Optional[List[str]]: + def swing_modes(self) -> list[str] | None: """Return the list of available swing modes. Requires SUPPORT_SWING_MODE. diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 3f2b8dc23f2..02474a47f96 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Climate.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -38,7 +38,7 @@ SET_PRESET_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ACTION_SCHEMA = vol.Any(SET_HVAC_MODE_SCHEMA, SET_PRESET_MODE_SCHEMA) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Climate devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -76,11 +76,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if config[CONF_TYPE] == "set_hvac_mode": diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 423efdf8196..d20c202e93b 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -1,5 +1,5 @@ """Provide the device automations for Climate.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -42,7 +42,7 @@ CONDITION_SCHEMA = vol.Any(HVAC_MODE_CONDITION, PRESET_MODE_CONDITION) async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Climate devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] @@ -119,6 +119,6 @@ async def async_get_condition_capabilities(hass, config): else: preset_modes = [] - fields[vol.Required(const.ATTR_PRESET_MODES)] = vol.In(preset_modes) + fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes) return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 84a7c35162a..df925463d4c 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Climate.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -58,7 +58,7 @@ CURRENT_TRIGGER_SCHEMA = vol.All( TRIGGER_SCHEMA = vol.Any(HVAC_MODE_TRIGGER_SCHEMA, CURRENT_TRIGGER_SCHEMA) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Climate devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -71,12 +71,16 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: state = hass.states.get(entry.entity_id) # Add triggers for each entity that belongs to this integration + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "hvac_mode_changed", } ) @@ -84,10 +88,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: if state and const.ATTR_CURRENT_TEMPERATURE in state.attributes: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "current_temperature_changed", } ) @@ -95,10 +96,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: if state and const.ATTR_CURRENT_HUMIDITY in state.attributes: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "current_humidity_changed", } ) @@ -113,7 +111,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) trigger_type = config[CONF_TYPE] if trigger_type == "hvac_mode_changed": diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py index 87674da414b..3603e37d970 100644 --- a/homeassistant/components/climate/group.py +++ b/homeassistant/components/climate/group.py @@ -3,15 +3,14 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback from .const import HVAC_MODE_OFF, HVAC_MODES @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/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 1217d5fde4c..be52138e3e5 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -1,10 +1,11 @@ """Module that groups code required to handle state restore for component.""" +from __future__ import annotations + import asyncio -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ATTR_TEMPERATURE -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from .const import ( ATTR_AUX_HEAT, @@ -26,11 +27,11 @@ from .const import ( async def _async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" @@ -73,11 +74,11 @@ async def _async_reproduce_states( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" await asyncio.gather( diff --git a/homeassistant/components/climate/translations/id.json b/homeassistant/components/climate/translations/id.json index 4f1ec02379b..bdae7d60067 100644 --- a/homeassistant/components/climate/translations/id.json +++ b/homeassistant/components/climate/translations/id.json @@ -1,13 +1,28 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "Ubah mode HVAC di {entity_name}", + "set_preset_mode": "Ubah prasetel di {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} disetel ke mode HVAC tertentu", + "is_preset_mode": "{entity_name} disetel ke mode prasetel tertentu" + }, + "trigger_type": { + "current_humidity_changed": "Kelembaban terukur {entity_name} berubah", + "current_temperature_changed": "Suhu terukur {entity_name} berubah", + "hvac_mode_changed": "Mode HVAC {entity_name} berubah" + } + }, "state": { "_": { - "auto": "Auto", - "cool": "Sejuk", + "auto": "Otomatis", + "cool": "Dingin", "dry": "Kering", "fan_only": "Hanya kipas", "heat": "Panas", "heat_cool": "Panas/Dingin", - "off": "Off" + "off": "Mati" } }, "title": "Cuaca" diff --git a/homeassistant/components/climate/translations/ko.json b/homeassistant/components/climate/translations/ko.json index 0923d166040..7c7342ef95c 100644 --- a/homeassistant/components/climate/translations/ko.json +++ b/homeassistant/components/climate/translations/ko.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "{entity_name} \uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd", - "set_preset_mode": "{entity_name} \uc758 \ud504\ub9ac\uc14b \ubcc0\uacbd" + "set_hvac_mode": "{entity_name}\uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd\ud558\uae30", + "set_preset_mode": "{entity_name}\uc758 \ud504\ub9ac\uc14b \ubcc0\uacbd\ud558\uae30" }, "condition_type": { - "is_hvac_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", - "is_preset_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \ud504\ub9ac\uc14b \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74" + "is_hvac_mode": "{entity_name}\uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", + "is_preset_mode": "{entity_name}\uc774(\uac00) \ud2b9\uc815 \ud504\ub9ac\uc14b \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74" }, "trigger_type": { - "current_humidity_changed": "{entity_name} \uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", - "current_temperature_changed": "{entity_name} \uc774(\uac00) \uc628\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", - "hvac_mode_changed": "{entity_name} HVAC \ubaa8\ub4dc\uac00 \ubcc0\uacbd\ub420 \ub54c" + "current_humidity_changed": "{entity_name}\uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud588\uc744 \ub54c", + "current_temperature_changed": "{entity_name}\uc774(\uac00) \uc628\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud588\uc744 \ub54c", + "hvac_mode_changed": "{entity_name}\uc758 HVAC \ubaa8\ub4dc\uac00 \ubcc0\uacbd\ub418\uc5c8\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/climate/translations/zh-Hans.json b/homeassistant/components/climate/translations/zh-Hans.json index 9927cd679ae..a93125525e1 100644 --- a/homeassistant/components/climate/translations/zh-Hans.json +++ b/homeassistant/components/climate/translations/zh-Hans.json @@ -5,8 +5,8 @@ "set_preset_mode": "\u66f4\u6539 {entity_name} \u9884\u8bbe\u6a21\u5f0f" }, "condition_type": { - "is_hvac_mode": "{entity_name} \u88ab\u8bbe\u4e3a\u6307\u5b9a\u7684\u7a7a\u8c03\u6a21\u5f0f", - "is_preset_mode": "{entity_name} \u88ab\u8bbe\u4e3a\u6307\u5b9a\u7684\u9884\u8bbe\u6a21\u5f0f" + "is_hvac_mode": "{entity_name} \u5df2\u8bbe\u4e3a\u6307\u5b9a\u7684\u7a7a\u8c03\u6a21\u5f0f", + "is_preset_mode": "{entity_name} \u5df2\u8bbe\u4e3a\u6307\u5b9a\u7684\u9884\u8bbe\u6a21\u5f0f" }, "trigger_type": { "current_humidity_changed": "{entity_name} \u6d4b\u91cf\u7684\u5ba4\u5185\u6e7f\u5ea6\u53d8\u5316", diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index b1ad55c0b04..038bc227fcd 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.alexa import const as alexa_const from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( + CONF_DESCRIPTION, CONF_MODE, CONF_NAME, CONF_REGION, @@ -49,7 +50,7 @@ SERVICE_REMOTE_DISCONNECT = "remote_disconnect" ALEXA_ENTITY_SCHEMA = vol.Schema( { - vol.Optional(alexa_const.CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_DESCRIPTION): cv.string, vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string, vol.Optional(CONF_NAME): cv.string, } diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 2d4714b4c81..138b2db0b8c 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -1,5 +1,6 @@ """Alexa configuration for Home Assistant Cloud.""" import asyncio +from contextlib import suppress from datetime import timedelta import logging @@ -322,7 +323,5 @@ class AlexaConfig(alexa_config.AbstractConfig): if "old_entity_id" in event.data: to_remove.append(event.data["old_entity_id"]) - try: + with suppress(alexa_errors.NoTokenAvailable): await self._sync_helper(to_update, to_remove) - except alexa_errors.NoTokenAvailable: - pass diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 155a39e49b6..f451a4faddb 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -1,8 +1,10 @@ """Interface implementation for cloud client.""" +from __future__ import annotations + import asyncio import logging from pathlib import Path -from typing import Any, Dict +from typing import Any import aiohttp from hass_nabucasa.client import CloudClient as Interface @@ -13,10 +15,9 @@ from homeassistant.components.alexa import ( ) from homeassistant.components.google_assistant import const as gc, smart_home as ga from homeassistant.const import HTTP_OK -from homeassistant.core import Context, callback +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.aiohttp import MockRequest from . import alexa_config, google_config, utils @@ -29,11 +30,11 @@ class CloudClient(Interface): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, prefs: CloudPreferences, websession: aiohttp.ClientSession, - alexa_user_config: Dict[str, Any], - google_user_config: Dict[str, Any], + alexa_user_config: dict[str, Any], + google_user_config: dict[str, Any], ): """Initialize client interface to Cloud.""" self._hass = hass @@ -70,7 +71,7 @@ class CloudClient(Interface): return self._hass.http.runner @property - def cloudhooks(self) -> Dict[str, Dict[str, str]]: + def cloudhooks(self) -> dict[str, dict[str, str]]: """Return list of cloudhooks.""" return self._prefs.cloudhooks @@ -108,7 +109,7 @@ class CloudClient(Interface): async def logged_in(self) -> None: """When user logs in.""" - await self.prefs.async_set_username(self.cloud.username) + is_new_user = await self.prefs.async_set_username(self.cloud.username) async def enable_alexa(_): """Enable Alexa.""" @@ -134,6 +135,9 @@ class CloudClient(Interface): if gconf.should_report_state: gconf.async_enable_report_state() + if is_new_user: + await gconf.async_sync_entities(gconf.agent_user_id) + tasks = [] if self._prefs.alexa_enabled and self._prefs.alexa_report_state: @@ -164,7 +168,7 @@ class CloudClient(Interface): if identifier.startswith("remote_"): async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data) - async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: + async def async_alexa_message(self, payload: dict[Any, Any]) -> dict[Any, Any]: """Process cloud alexa message to client.""" cloud_user = await self._prefs.get_cloud_user() aconfig = await self.get_alexa_config() @@ -176,7 +180,7 @@ class CloudClient(Interface): enabled=self._prefs.alexa_enabled, ) - async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: + async def async_google_message(self, payload: dict[Any, Any]) -> dict[Any, Any]: """Process cloud google message to client.""" if not self._prefs.google_enabled: return ga.turned_off_response(payload) @@ -187,7 +191,7 @@ class CloudClient(Interface): self._hass, gconf, gconf.cloud_user, payload, gc.SOURCE_CLOUD ) - async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: + async def async_webhook_message(self, payload: dict[Any, Any]) -> dict[Any, Any]: """Process cloud webhook message to client.""" cloudhook_id = payload["cloudhook_id"] @@ -221,6 +225,6 @@ class CloudClient(Interface): "headers": {"Content-Type": response.content_type}, } - async def async_cloudhooks_update(self, data: Dict[str, Dict[str, str]]) -> None: + async def async_cloudhooks_update(self, data: dict[str, dict[str, str]]) -> None: """Update local list of cloudhooks.""" await self._prefs.async_update(cloudhooks=data) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index dffa1e2f306..62ca1b15a71 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -6,11 +6,7 @@ from hass_nabucasa import Cloud, cloud_api from hass_nabucasa.google_report_state import ErrorResponse from homeassistant.components.google_assistant.helpers import AbstractConfig -from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, - EVENT_HOMEASSISTANT_STARTED, - HTTP_OK, -) +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK from homeassistant.core import CoreState, split_entity_id from homeassistant.helpers import entity_registry @@ -87,8 +83,15 @@ class CloudGoogleConfig(AbstractConfig): async def async_initialize(self): """Perform async initialization of config.""" await super().async_initialize() - # Remove bad data that was there until 0.103.6 - Jan 6, 2020 - self._store.pop_agent_user_id(self._user) + + # Remove old/wrong user agent ids + remove_agent_user_ids = [] + for agent_user_id in self._store.agent_user_ids: + if agent_user_id != self.agent_user_id: + remove_agent_user_ids.append(agent_user_id) + + for agent_user_id in remove_agent_user_ids: + await self.async_disconnect_agent_user(agent_user_id) self._prefs.async_listen_updates(self._async_prefs_updated) @@ -128,6 +131,11 @@ class CloudGoogleConfig(AbstractConfig): """Return Agent User Id to use for query responses.""" return self._cloud.username + @property + def has_registered_user_agent(self): + """Return if we have a Agent User Id registered.""" + return len(self._store.agent_user_ids) > 0 + def get_agent_user_id(self, context): """Get agent user ID making request.""" return self.agent_user_id @@ -198,17 +206,7 @@ class CloudGoogleConfig(AbstractConfig): if not self._should_expose_entity_id(entity_id): return - if self.hass.state == CoreState.running: - self.async_schedule_google_sync_all() + if self.hass.state != CoreState.running: return - if self._sync_on_started: - return - - self._sync_on_started = True - - async def sync_google(_): - """Sync entities to Google.""" - await self.async_sync_entities_all() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, sync_google) + self.async_schedule_google_sync_all() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 2bcc37fec05..e9771012379 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -27,7 +27,6 @@ from homeassistant.const import ( HTTP_OK, HTTP_UNAUTHORIZED, ) -from homeassistant.core import callback from .const import ( DOMAIN, @@ -225,6 +224,7 @@ class CloudLoginView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] await cloud.login(data["email"], data["password"]) + return self.json({"success": True}) @@ -310,15 +310,15 @@ class CloudForgotPasswordView(HomeAssistantView): return self.json_message("ok") -@callback -def websocket_cloud_status(hass, connection, msg): +@websocket_api.async_response +async def websocket_cloud_status(hass, connection, msg): """Handle request for account info. Async friendly. """ cloud = hass.data[DOMAIN] connection.send_message( - websocket_api.result_message(msg["id"], _account_data(cloud)) + websocket_api.result_message(msg["id"], await _account_data(cloud)) ) @@ -446,7 +446,7 @@ async def websocket_hook_delete(hass, connection, msg): connection.send_message(websocket_api.result_message(msg["id"])) -def _account_data(cloud): +async def _account_data(cloud): """Generate the auth data JSON response.""" if not cloud.is_logged_in: @@ -456,6 +456,8 @@ def _account_data(cloud): client = cloud.client remote = cloud.remote + gconf = await client.get_google_config() + # Load remote certificate if remote.certificate: certificate = attr.asdict(remote.certificate) @@ -467,6 +469,7 @@ def _account_data(cloud): "email": claims["email"], "cloud": cloud.iot.state, "prefs": client.prefs.as_dict(), + "google_registered": gconf.has_registered_user_agent, "google_entities": client.google_user_config["filter"].config, "alexa_entities": client.alexa_user_config["filter"].config, "remote_domain": remote.instance_domain, @@ -485,7 +488,7 @@ async def websocket_remote_connect(hass, connection, msg): cloud = hass.data[DOMAIN] await cloud.client.prefs.async_update(remote_enabled=True) await cloud.remote.connect() - connection.send_result(msg["id"], _account_data(cloud)) + connection.send_result(msg["id"], await _account_data(cloud)) @websocket_api.require_admin @@ -498,7 +501,7 @@ async def websocket_remote_disconnect(hass, connection, msg): cloud = hass.data[DOMAIN] await cloud.client.prefs.async_update(remote_enabled=False) await cloud.remote.disconnect() - connection.send_result(msg["id"], _account_data(cloud)) + connection.send_result(msg["id"], await _account_data(cloud)) @websocket_api.require_admin diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 9d27de13309..b854cb4578d 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.41.0"], + "requirements": ["hass-nabucasa==0.42.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a15eafc4d08..c51d5278730 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,6 +1,7 @@ """Preference management for cloud.""" +from __future__ import annotations + from ipaddress import ip_address -from typing import List, Optional from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User @@ -172,7 +173,7 @@ class CloudPreferences: updated_entities = {**entities, entity_id: updated_entity} await self.async_update(alexa_entity_configs=updated_entities) - async def async_set_username(self, username): + async def async_set_username(self, username) -> bool: """Set the username that is logged in.""" # Logging out. if username is None: @@ -181,18 +182,20 @@ class CloudPreferences: if user is not None: await self._hass.auth.async_remove_user(user) await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None}) - return + return False cur_username = self._prefs.get(PREF_USERNAME) if cur_username == username: - return + return False if cur_username is None: await self._save_prefs({**self._prefs, PREF_USERNAME: username}) else: await self._save_prefs(self._empty_config(username)) + return True + def as_dict(self): """Return dictionary version.""" return { @@ -234,7 +237,7 @@ class CloudPreferences: return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE) @property - def alexa_default_expose(self) -> Optional[List[str]]: + def alexa_default_expose(self) -> list[str] | None: """Return array of entity domains that are exposed by default to Alexa. Can return None, in which case for backwards should be interpreted as allow all domains. @@ -272,7 +275,7 @@ class CloudPreferences: return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID] @property - def google_default_expose(self) -> Optional[List[str]]: + def google_default_expose(self) -> list[str] | None: """Return array of entity domains that are exposed by default to Google. Can return None, in which case for backwards should be interpreted as allow all domains. @@ -302,7 +305,7 @@ class CloudPreferences: await self.async_update(cloud_user=user.id) return user.id - async def _load_cloud_user(self) -> Optional[User]: + async def _load_cloud_user(self) -> User | None: """Load cloud user if available.""" user_id = self._prefs.get(PREF_CLOUD_USER) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index 6c069ce16d7..80578a8d721 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -1,5 +1,5 @@ """Support for the cloud for speech to text service.""" -from typing import List +from __future__ import annotations from aiohttp import StreamReader from hass_nabucasa import Cloud @@ -56,32 +56,32 @@ class CloudProvider(Provider): self.cloud = cloud @property - def supported_languages(self) -> List[str]: + def supported_languages(self) -> list[str]: """Return a list of supported languages.""" return SUPPORT_LANGUAGES @property - def supported_formats(self) -> List[AudioFormats]: + def supported_formats(self) -> list[AudioFormats]: """Return a list of supported formats.""" return [AudioFormats.WAV, AudioFormats.OGG] @property - def supported_codecs(self) -> List[AudioCodecs]: + def supported_codecs(self) -> list[AudioCodecs]: """Return a list of supported codecs.""" return [AudioCodecs.PCM, AudioCodecs.OPUS] @property - def supported_bit_rates(self) -> List[AudioBitRates]: + def supported_bit_rates(self) -> list[AudioBitRates]: """Return a list of supported bitrates.""" return [AudioBitRates.BITRATE_16] @property - def supported_sample_rates(self) -> List[AudioSampleRates]: + def supported_sample_rates(self) -> list[AudioSampleRates]: """Return a list of supported samplerates.""" return [AudioSampleRates.SAMPLERATE_16000] @property - def supported_channels(self) -> List[AudioChannels]: + def supported_channels(self) -> list[AudioChannels]: """Return a list of supported channels.""" return [AudioChannels.CHANNEL_MONO] diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json index a2bea167b5e..8301806831b 100644 --- a/homeassistant/components/cloud/translations/hu.json +++ b/homeassistant/components/cloud/translations/hu.json @@ -2,6 +2,8 @@ "system_health": { "info": { "alexa_enabled": "Alexa enged\u00e9lyezve", + "can_reach_cert_server": "Tan\u00fas\u00edtv\u00e1nykiszolg\u00e1l\u00f3 el\u00e9r\u00e9se", + "can_reach_cloud": "Home Assistant Cloud el\u00e9r\u00e9se", "can_reach_cloud_auth": "Hiteles\u00edt\u00e9si kiszolg\u00e1l\u00f3 el\u00e9r\u00e9se", "google_enabled": "Google enged\u00e9lyezve", "logged_in": "Bejelentkezve", diff --git a/homeassistant/components/cloud/translations/id.json b/homeassistant/components/cloud/translations/id.json new file mode 100644 index 00000000000..1cff542796c --- /dev/null +++ b/homeassistant/components/cloud/translations/id.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa Diaktifkan", + "can_reach_cert_server": "Keterjangkauan Server Sertifikat", + "can_reach_cloud": "Keterjangkauan Home Assistant Cloud", + "can_reach_cloud_auth": "Keterjangkauan Server Autentikasi", + "google_enabled": "Google Diaktifkan", + "logged_in": "Masuk", + "relayer_connected": "Relayer Terhubung", + "remote_connected": "Terhubung Jarak Jauh", + "remote_enabled": "Kontrol Jarak Jauh Diaktifkan", + "subscription_expiration": "Masa Kedaluwarsa Langganan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/ko.json b/homeassistant/components/cloud/translations/ko.json new file mode 100644 index 00000000000..269afab2ce9 --- /dev/null +++ b/homeassistant/components/cloud/translations/ko.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa \ud65c\uc131\ud654", + "can_reach_cert_server": "\uc778\uc99d\uc11c \uc11c\ubc84 \uc5f0\uacb0", + "can_reach_cloud": "Home Assistant Cloud \uc5f0\uacb0", + "can_reach_cloud_auth": "\uc778\uc99d \uc11c\ubc84 \uc5f0\uacb0", + "google_enabled": "Google Assistant \ud65c\uc131\ud654", + "logged_in": "\ub85c\uadf8\uc778", + "relayer_connected": "\uc911\uacc4\uae30 \uc5f0\uacb0", + "remote_connected": "\uc6d0\uaca9 \uc81c\uc5b4 \uc5f0\uacb0", + "remote_enabled": "\uc6d0\uaca9 \uc81c\uc5b4 \ud65c\uc131\ud654", + "subscription_expiration": "\uad6c\ub3c5 \ub9cc\ub8cc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/nl.json b/homeassistant/components/cloud/translations/nl.json index d9aa78afecb..0ad7a528822 100644 --- a/homeassistant/components/cloud/translations/nl.json +++ b/homeassistant/components/cloud/translations/nl.json @@ -2,6 +2,7 @@ "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", "google_enabled": "Google ingeschakeld", diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 36599b42ad3..57f84b057f7 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -1,10 +1,12 @@ """Helper functions for cloud components.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any from aiohttp import payload, web -def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: +def aiohttp_serialize_response(response: web.Response) -> dict[str, Any]: """Serialize an aiohttp response to a dictionary.""" body = response.body diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 446890887c1..abef32a4c5c 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -1,7 +1,8 @@ """Update the IP addresses of your Cloudflare DNS records.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Dict from pycfdns import CloudflareUpdater from pycfdns.exceptions import ( @@ -51,7 +52,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: Dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the component.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index 066fff9f704..0e3468903af 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Cloudflare integration.""" +from __future__ import annotations + import logging -from typing import Dict, List, Optional from pycfdns import CloudflareUpdater from pycfdns.exceptions import ( @@ -18,8 +19,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_RECORDS -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_RECORDS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,7 +30,7 @@ DATA_SCHEMA = vol.Schema( ) -def _zone_schema(zones: Optional[List] = None): +def _zone_schema(zones: list | None = None): """Zone selection schema.""" zones_list = [] @@ -40,7 +40,7 @@ def _zone_schema(zones: Optional[List] = None): return vol.Schema({vol.Required(CONF_ZONE): vol.In(zones_list)}) -def _records_schema(records: Optional[List] = None): +def _records_schema(records: list | None = None): """Zone records selection schema.""" records_dict = {} @@ -50,7 +50,7 @@ def _records_schema(records: Optional[List] = None): return vol.Schema({vol.Required(CONF_RECORDS): cv.multi_select(records_dict)}) -async def validate_input(hass: HomeAssistant, data: Dict): +async def validate_input(hass: HomeAssistant, data: dict): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -92,7 +92,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): self.zones = None self.records = None - async def async_step_user(self, user_input: Optional[Dict] = None): + async def async_step_user(self, user_input: dict | None = None): """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -113,7 +113,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_zone(self, user_input: Optional[Dict] = None): + async def async_step_zone(self, user_input: dict | None = None): """Handle the picking the zone.""" errors = {} @@ -133,7 +133,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_records(self, user_input: Optional[Dict] = None): + async def async_step_records(self, user_input: dict | None = None): """Handle the picking the zone records.""" errors = {} diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json index fa13d00617f..fed6f22d536 100644 --- a/homeassistant/components/cloudflare/translations/hu.json +++ b/homeassistant/components/cloudflare/translations/hu.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "unknown": "V\u00e1ratlan hiba" + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_zone": "\u00c9rv\u00e9nytelen z\u00f3na" }, "flow_title": "Cloudflare: {name}", diff --git a/homeassistant/components/cloudflare/translations/id.json b/homeassistant/components/cloudflare/translations/id.json new file mode 100644 index 00000000000..98286398ea8 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_zone": "Zona tidak valid" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "Catatan" + }, + "title": "Pilih Catatan untuk Diperbarui" + }, + "user": { + "data": { + "api_token": "Token API" + }, + "description": "Integrasi ini memerlukan Token API yang dibuat dengan izin Zone:Zone:Read and Zone:DNS:Edit untuk semua zona di akun Anda.", + "title": "Hubungkan ke Cloudflare" + }, + "zone": { + "data": { + "zone": "Zona" + }, + "title": "Pilih Zona yang akan Diperbarui" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/ko.json b/homeassistant/components/cloudflare/translations/ko.json index d4f4eee49a8..4dbe263a138 100644 --- a/homeassistant/components/cloudflare/translations/ko.json +++ b/homeassistant/components/cloudflare/translations/ko.json @@ -1,18 +1,34 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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.", "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_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_zone": "\uc601\uc5ed\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Cloudflare: {name}", "step": { + "records": { + "data": { + "records": "\ub808\ucf54\ub4dc" + }, + "title": "\uc5c5\ub370\uc774\ud2b8\ud560 \ub808\ucf54\ub4dc \uc120\ud0dd\ud558\uae30" + }, "user": { "data": { "api_token": "API \ud1a0\ud070" - } + }, + "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc758 \ubaa8\ub4e0 \uc601\uc5ed\uc5d0 \ub300\ud574 Zone:Zone:Read \ubc0f Zone:DNS:Edit \uad8c\ud55c\uc73c\ub85c \uc0dd\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.", + "title": "Cloudflare\uc5d0 \uc5f0\uacb0\ud558\uae30" + }, + "zone": { + "data": { + "zone": "\uc601\uc5ed" + }, + "title": "\uc5c5\ub370\uc774\ud2b8\ud560 \uc601\uc5ed \uc120\ud0dd\ud558\uae30" } } } diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json index 35c765d5da7..94697419ff1 100644 --- a/homeassistant/components/cloudflare/translations/nl.json +++ b/homeassistant/components/cloudflare/translations/nl.json @@ -21,12 +21,14 @@ "data": { "api_token": "API-token" }, + "description": "Voor deze integratie is een API-token vereist dat is gemaakt met Zone:Zone:Lezen en Zone:DNS:Bewerk machtigingen voor alle zones in uw account.", "title": "Verbinden met Cloudflare" }, "zone": { "data": { "zone": "Zone" - } + }, + "title": "Kies de zone die u wilt bijwerken" } } } diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 49c10ab92a5..3968ebbe9d7 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -98,7 +98,6 @@ class CmusRemote: class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" - # pylint: disable=no-member def __init__(self, device, name, server): """Initialize the CMUS device.""" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index a61615d0e23..c7d2a64d6b0 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -4,7 +4,7 @@ import logging import CO2Signal import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, @@ -13,7 +13,6 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity CONF_COUNTRY_CODE = "country_code" @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs, True) -class CO2Sensor(Entity): +class CO2Sensor(SensorEntity): """Implementation of the CO2Signal sensor.""" def __init__(self, token, country_code, lat, lon): @@ -91,7 +90,7 @@ class CO2Sensor(Entity): return CO2_INTENSITY_UNIT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the last update.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index f191bb778f4..e4e4e719c9e 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -1,6 +1,6 @@ """Support for Coinbase sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.entity import Entity ATTR_NATIVE_BALANCE = "Balance in native currency" @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([sensor], True) -class AccountSensor(Entity): +class AccountSensor(SensorEntity): """Representation of a Coinbase.com sensor.""" def __init__(self, coinbase_data, name, currency): @@ -71,7 +71,7 @@ class AccountSensor(Entity): return CURRENCY_ICONS.get(self._unit_of_measurement, DEFAULT_COIN_ICON) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -88,7 +88,7 @@ class AccountSensor(Entity): self._native_currency = account["native_balance"]["currency"] -class ExchangeRateSensor(Entity): +class ExchangeRateSensor(SensorEntity): """Representation of a Coinbase.com sensor.""" def __init__(self, coinbase_data, exchange_currency, native_currency): @@ -120,7 +120,7 @@ class ExchangeRateSensor(Entity): return CURRENCY_ICONS.get(self.currency, DEFAULT_COIN_ICON) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 90830d52236..5d4ec6eec13 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -8,11 +8,10 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://hourlypricing.comed.com/api" @@ -65,7 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class ComedHourlyPricingSensor(Entity): +class ComedHourlyPricingSensor(SensorEntity): """Implementation of a ComEd Hourly Pricing sensor.""" def __init__(self, loop, websession, sensor_type, offset, name): @@ -97,7 +96,7 @@ class ComedHourlyPricingSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 26abd85522a..53bc242ba2f 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,4 +1,6 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" +from __future__ import annotations + import logging import math @@ -95,7 +97,7 @@ class ComfoConnectFan(FanEntity): return SUPPORT_SET_SPEED @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed percentage.""" speed = self._ccb.data.get(SENSOR_FAN_SPEED_MODE) if speed is None: diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 87fa8f4a1a6..728bc13b76b 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -26,7 +26,7 @@ from pycomfoconnect import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -45,7 +45,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge @@ -258,7 +257,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class ComfoConnectSensor(Entity): +class ComfoConnectSensor(SensorEntity): """Representation of a ComfoConnect sensor.""" def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 05d2b9634f2..961d9a31f4e 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -107,11 +107,6 @@ class CommandCover(CoverEntity): return success - def _query_state_value(self, command): - """Execute state command for return value.""" - _LOGGER.info("Running state value command: %s", command) - return check_output_or_log(command, self._timeout) - @property def should_poll(self): """Only poll if we have state command.""" @@ -138,10 +133,8 @@ class CommandCover(CoverEntity): def _query_state(self): """Query for the state.""" - if not self._command_state: - _LOGGER.error("No state command specified") - return - return self._query_state_value(self._command_state) + _LOGGER.info("Running state value command: %s", self._command_state) + return check_output_or_log(self._command_state, self._timeout) def update(self): """Update device state.""" diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 35f7c5a4811..10c5a16f60b 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_COMMAND, CONF_NAME, @@ -17,7 +17,6 @@ from homeassistant.const import ( from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.reload import setup_reload_service from . import check_output_or_log @@ -63,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class CommandSensor(Entity): +class CommandSensor(SensorEntity): """Representation of a sensor that is using shell commands.""" def __init__( @@ -95,7 +94,7 @@ class CommandSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes @@ -145,19 +144,14 @@ class CommandSensorData: def update(self): """Get the latest data with a shell command.""" command = self.command - cache = {} - if command in cache: - prog, args, args_compiled = cache[command] - elif " " not in command: + if " " not in command: prog = command args = None args_compiled = None - cache[command] = (prog, args, args_compiled) else: prog, args = command.split(" ", 1) args_compiled = template.Template(args, self.hass) - cache[command] = (prog, args, args_compiled) if args_compiled: try: diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index ce46cd4f2cd..ae6c1c0c925 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -144,9 +144,6 @@ class CommandSwitch(SwitchEntity): def _query_state(self): """Query for state.""" - if not self._command_state: - _LOGGER.error("No state command specified") - return if self._value_template: return self._query_state_value(self._command_state) return self._query_state_code(self._command_state) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b45b6abe468..edf94268741 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -97,6 +97,17 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView): return self.json({"require_restart": not result}) +def _prepare_config_flow_result_json(result, prepare_result_json): + """Convert result to JSON.""" + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return prepare_result_json(result) + + data = result.copy() + data["result"] = entry_json(result["result"]) + data.pop("data") + return data + + class ConfigManagerFlowIndexView(FlowManagerIndexView): """View to create config flows.""" @@ -118,13 +129,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): def _prepare_result_json(self, result): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return super()._prepare_result_json(result) - - data = result.copy() - data["result"] = data["result"].entry_id - data.pop("data") - return data + return _prepare_config_flow_result_json(result, super()._prepare_result_json) class ConfigManagerFlowResourceView(FlowManagerResourceView): @@ -151,13 +156,7 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView): def _prepare_result_json(self, result): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return super()._prepare_result_json(result) - - data = result.copy() - data["result"] = data["result"].entry_id - data.pop("data") - return data + return _prepare_config_flow_result_json(result, super()._prepare_result_json) class ConfigManagerAvailableFlowView(HomeAssistantView): diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 22a9bf4f02a..8319816eb8a 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -6,9 +6,8 @@ from homeassistant.components.group import ( ) from homeassistant.config import GROUP_CONFIG_PATH from homeassistant.const import SERVICE_RELOAD -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 . import EditKeyBasedConfigView @@ -35,7 +34,7 @@ async def async_setup(hass): @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" return diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 9ddd9d76ed9..e988e58f76b 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -6,6 +6,7 @@ This will return a request id that has to be used for future calls. A callback has to be provided to `request_config` which will be called when the user has submitted configuration information. """ +from contextlib import suppress import functools as ft from homeassistant.const import ( @@ -96,11 +97,8 @@ def request_config(hass, *args, **kwargs): @async_callback def async_notify_errors(hass, request_id, error): """Add errors to a config request.""" - try: + with suppress(KeyError): # If request_id does not exist hass.data[DATA_REQUESTS][request_id].async_notify_errors(request_id, error) - except KeyError: - # If request_id does not exist - pass @bind_hass @@ -115,11 +113,8 @@ def notify_errors(hass, request_id, error): @async_callback def async_request_done(hass, request_id): """Mark a configuration request as done.""" - try: + with suppress(KeyError): # If request_id does not exist hass.data[DATA_REQUESTS].pop(request_id).async_request_done(request_id) - except KeyError: - # If request_id does not exist - pass @bind_hass diff --git a/homeassistant/components/configurator/translations/id.json b/homeassistant/components/configurator/translations/id.json index 759af513228..f345a39417b 100644 --- a/homeassistant/components/configurator/translations/id.json +++ b/homeassistant/components/configurator/translations/id.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Konfigurasi", + "configure": "Konfigurasikan", "configured": "Terkonfigurasi" } }, diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index a45cdc4006a..d7f8ec52f7a 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -42,17 +42,9 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["light"] -async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Stub to allow setting up this component. - - Configuration through YAML is not supported at this time. - """ - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Control4 from a config entry.""" + hass.data.setdefault(DOMAIN, {}) entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {}) account_session = aiohttp_client.async_get_clientsession(hass) @@ -115,9 +107,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -134,8 +126,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 03183edbfda..1456c440dfa 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -19,8 +19,12 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.device_registry import format_mac -from .const import CONF_CONTROLLER_UNIQUE_ID, DEFAULT_SCAN_INTERVAL, MIN_SCAN_INTERVAL -from .const import DOMAIN # pylint:disable=unused-import +from .const import ( + CONF_CONTROLLER_UNIQUE_ID, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MIN_SCAN_INTERVAL, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json index 399b8d42491..e50e2499320 100644 --- a/homeassistant/components/control4/translations/de.json +++ b/homeassistant/components/control4/translations/de.json @@ -14,6 +14,16 @@ "host": "IP-Addresse", "password": "Passwort", "username": "Benutzername" + }, + "description": "Bitte gib deine Control4-Kontodaten und die IP-Adresse deiner lokalen Steuerung ein." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunden zwischen Updates" } } } diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json index 3b2d79a34a7..68cb4fe23a9 100644 --- a/homeassistant/components/control4/translations/hu.json +++ b/homeassistant/components/control4/translations/hu.json @@ -2,6 +2,20 @@ "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" + }, + "step": { + "user": { + "data": { + "host": "IP c\u00edm", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/id.json b/homeassistant/components/control4/translations/id.json new file mode 100644 index 00000000000..4b8033c0873 --- /dev/null +++ b/homeassistant/components/control4/translations/id.json @@ -0,0 +1,31 @@ +{ + "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": "Alamat IP", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan detail akun Control4 Anda dan alamat IP pengontrol lokal Anda." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pembaruan dalam detik" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/ko.json b/homeassistant/components/control4/translations/ko.json index ca36da40c18..245fe666eab 100644 --- a/homeassistant/components/control4/translations/ko.json +++ b/homeassistant/components/control4/translations/ko.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9(\ucd08)" + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08)" } } } diff --git a/homeassistant/components/control4/translations/nl.json b/homeassistant/components/control4/translations/nl.json index 1c4e7de05c9..f13dd5e7f64 100644 --- a/homeassistant/components/control4/translations/nl.json +++ b/homeassistant/components/control4/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -13,6 +14,16 @@ "host": "IP-adres", "password": "Wachtwoord", "username": "Gebruikersnaam" + }, + "description": "Voer uw Control4-accountgegevens en het IP-adres van uw lokale controller in." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconden tussen updates" } } } diff --git a/homeassistant/components/control4/translations/ru.json b/homeassistant/components/control4/translations/ru.json index 3882f03cb32..41a033c0376 100644 --- a/homeassistant/components/control4/translations/ru.json +++ b/homeassistant/components/control4/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Control4 \u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430." } diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index c9c2ab46cf9..56cf4aecdea 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -1,6 +1,7 @@ """Agent foundation for conversation integration.""" +from __future__ import annotations + from abc import ABC, abstractmethod -from typing import Optional from homeassistant.core import Context from homeassistant.helpers import intent @@ -24,6 +25,6 @@ class AbstractConversationAgent(ABC): @abstractmethod async def async_process( - self, text: str, context: Context, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: str | None = None ) -> intent.IntentResponse: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index d2dcf13a62a..a98f685ea1d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,6 +1,7 @@ """Standard conversastion implementation for Home Assistant.""" +from __future__ import annotations + import re -from typing import Optional from homeassistant import core, setup from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER @@ -112,7 +113,7 @@ class DefaultAgent(AbstractConversationAgent): async_register(self.hass, intent_type, sentences) async def async_process( - self, text: str, context: core.Context, conversation_id: Optional[str] = None + self, text: str, context: core.Context, conversation_id: str | None = None ) -> intent.IntentResponse: """Process a sentence.""" intents = self.hass.data[DOMAIN] diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 14165bd93b3..2b092935bb0 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -13,12 +13,6 @@ from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up Coolmaster components.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" host = entry.data[CONF_HOST] @@ -31,7 +25,8 @@ async def async_setup_entry(hass, entry): except (OSError, ConnectionRefusedError, TimeoutError) as error: raise ConfigEntryNotReady() from error coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster) - await coordinator.async_refresh() + hass.data.setdefault(DOMAIN, {}) + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { DATA_INFO: info, DATA_COORDINATOR: coordinator, diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 7c9b4d5d065..04ee995d25c 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_PORT -# pylint: disable=unused-import from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} diff --git a/homeassistant/components/coolmaster/translations/id.json b/homeassistant/components/coolmaster/translations/id.json new file mode 100644 index 00000000000..d12c10da25a --- /dev/null +++ b/homeassistant/components/coolmaster/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "no_units": "Tidak dapat menemukan perangkat HVAC di host CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Mendukung mode dingin", + "dry": "Mendukung mode kering", + "fan_only": "Mendukung mode kipas saja", + "heat": "Mendukung mode panas", + "heat_cool": "Mendukung mode panas/dingin otomatis", + "host": "Host", + "off": "Bisa dimatikan" + }, + "title": "Siapkan detail koneksi CoolMasterNet Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index fa8efebe154..d05c4cef862 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -44,9 +44,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"]) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -57,8 +57,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index ccf2e7f2c75..6d2776c7ecc 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant import config_entries from . import get_coordinator -from .const import DOMAIN, OPTION_WORLDWIDE # pylint:disable=unused-import +from .const import DOMAIN, OPTION_WORLDWIDE class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 7f0e0c230e6..472b8bc8d1c 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for the Corona virus.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,7 +24,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class CoronavirusSensor(CoordinatorEntity): +class CoronavirusSensor(CoordinatorEntity, SensorEntity): """Sensor representing corona virus data.""" name = None @@ -73,6 +74,6 @@ class CoronavirusSensor(CoordinatorEntity): return "people" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/coronavirus/translations/hu.json b/homeassistant/components/coronavirus/translations/hu.json index fcee85c40e8..631454ec045 100644 --- a/homeassistant/components/coronavirus/translations/hu.json +++ b/homeassistant/components/coronavirus/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ez az orsz\u00e1g m\u00e1r konfigur\u00e1lva van." + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/id.json b/homeassistant/components/coronavirus/translations/id.json new file mode 100644 index 00000000000..e2626d16abb --- /dev/null +++ b/homeassistant/components/coronavirus/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "country": "Negara" + }, + "title": "Pilih negara untuk dipantau" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/nl.json b/homeassistant/components/coronavirus/translations/nl.json index d306894f7d0..fed3101b38e 100644 --- a/homeassistant/components/coronavirus/translations/nl.json +++ b/homeassistant/components/coronavirus/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dit land is al geconfigureerd." + "already_configured": "Service is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 868a74cc7b7..ecb405a81cd 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Dict, Optional import voluptuous as vol @@ -14,13 +13,13 @@ from homeassistant.const import ( CONF_MINIMUM, CONF_NAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent 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 _LOGGER = logging.getLogger(__name__) @@ -102,7 +101,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 counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -156,16 +155,16 @@ class CounterStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: Dict) -> Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return self.CREATE_SCHEMA(data) @callback - def _get_suggested_id(self, info: Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: Dict) -> Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return {**data, **update_data} @@ -174,14 +173,14 @@ class CounterStorageCollection(collection.StorageCollection): class Counter(RestoreEntity): """Representation of a counter.""" - def __init__(self, config: Dict): + def __init__(self, config: dict): """Initialize a counter.""" - self._config: Dict = config - self._state: Optional[int] = config[CONF_INITIAL] + self._config: dict = config + self._state: int | None = config[CONF_INITIAL] self.editable: bool = True @classmethod - def from_yaml(cls, config: Dict) -> Counter: + def from_yaml(cls, config: dict) -> Counter: """Create counter instance from yaml config.""" counter = cls(config) counter.editable = False @@ -194,22 +193,22 @@ class Counter(RestoreEntity): return False @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return name of the counter.""" return self._config.get(CONF_NAME) @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon to be used for this entity.""" return self._config.get(CONF_ICON) @property - def state(self) -> Optional[int]: + def state(self) -> int | None: """Return the current value of the counter.""" return self._state @property - def state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" ret = { ATTR_EDITABLE: self.editable, @@ -223,7 +222,7 @@ class Counter(RestoreEntity): return ret @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return unique id of the entity.""" return self._config[CONF_ID] @@ -276,7 +275,7 @@ class Counter(RestoreEntity): self._state = self.compute_next_state(new_state) self.async_write_ha_state() - async def async_update_config(self, config: Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Change the counter's settings WS CRUD.""" self._config = config self._state = self.compute_next_state(self._state) diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index b2dd63adedc..8fb15bd84e8 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -1,11 +1,12 @@ """Reproduce an Counter state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable 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_INITIAL, @@ -21,11 +22,11 @@ _LOGGER = logging.getLogger(__name__) async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -67,11 +68,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Counter states.""" await asyncio.gather( diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index c63963c87dc..034beb7f9db 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -2,7 +2,7 @@ from datetime import timedelta import functools as ft import logging -from typing import Any +from typing import Any, final import voluptuous as vol @@ -165,7 +165,7 @@ async def async_unload_entry(hass, entry): class CoverEntity(Entity): - """Representation of a cover.""" + """Base class for cover entities.""" @property def current_cover_position(self): @@ -196,6 +196,7 @@ class CoverEntity(Entity): return STATE_CLOSED if closed else STATE_OPEN + @final @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index 490ce162d9a..74eef8102df 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Cover.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -58,7 +58,7 @@ POSITION_ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ACTION_SCHEMA = vol.Any(CMD_ACTION_SCHEMA, POSITION_ACTION_SCHEMA) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Cover devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -153,7 +153,7 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di return { "extra_fields": vol.Schema( { - vol.Optional("position", default=0): vol.All( + vol.Optional(ATTR_POSITION, default=0): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ) } @@ -162,11 +162,9 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if config[CONF_TYPE] == "open": diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 0bcec2a6e43..2943f589f7b 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -1,5 +1,7 @@ """Provides device automations for Cover.""" -from typing import Any, Dict, List +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -65,10 +67,10 @@ STATE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( CONDITION_SCHEMA = vol.Any(POSITION_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device conditions for Cover devices.""" registry = await entity_registry.async_get_registry(hass) - conditions: List[Dict[str, Any]] = [] + conditions: list[dict[str, Any]] = [] # Get all the integrations entities for this device for entry in entity_registry.async_entries_for_device(registry, device_id): diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 764cc173e5f..9b94833bb29 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Cover.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, + CONF_FOR, CONF_PLATFORM, CONF_TYPE, CONF_VALUE_TEMPLATE, @@ -59,13 +60,14 @@ STATE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(STATE_TRIGGER_TYPES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) TRIGGER_SCHEMA = vol.Any(POSITION_TRIGGER_SCHEMA, STATE_TRIGGER_SCHEMA) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Cover devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -83,60 +85,32 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) # Add triggers for each entity that belongs to this integration + base_trigger = { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + } + if supports_open_close: - triggers.append( + triggers += [ { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "opened", + **base_trigger, + CONF_TYPE: trigger, } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "closed", - } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "opening", - } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "closing", - } - ) + for trigger in STATE_TRIGGER_TYPES + ] if supported_features & SUPPORT_SET_POSITION: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "position", } ) if supported_features & SUPPORT_SET_TILT_POSITION: triggers.append( { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, + **base_trigger, CONF_TYPE: "tilt_position", } ) @@ -146,8 +120,12 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - if config[CONF_TYPE] not in ["position", "tilt_position"]: - return {} + if config[CONF_TYPE] not in POSITION_TRIGGER_TYPES: + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } return { "extra_fields": vol.Schema( @@ -170,8 +148,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] in STATE_TRIGGER_TYPES: if config[CONF_TYPE] == "opened": to_state = STATE_OPEN @@ -187,6 +163,8 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_trigger.CONF_TO: to_state, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] state_config = state_trigger.TRIGGER_SCHEMA(state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py index d031b7cf693..28a1dc530fe 100644 --- a/homeassistant/components/cover/group.py +++ b/homeassistant/components/cover/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_CLOSED, STATE_OPEN -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.""" # On means open, Off means closed diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index 2a12172bdab..3b82596a21c 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Cover state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -22,8 +24,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -33,11 +34,11 @@ VALID_STATES = {STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING} async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -114,11 +115,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Cover states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/cover/translations/hu.json b/homeassistant/components/cover/translations/hu.json index 6d48cca1251..87bd1c241c6 100644 --- a/homeassistant/components/cover/translations/hu.json +++ b/homeassistant/components/cover/translations/hu.json @@ -6,7 +6,8 @@ "open": "{entity_name} nyit\u00e1sa", "open_tilt": "{entity_name} d\u00f6nt\u00e9s nyit\u00e1sa", "set_position": "{entity_name} poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa", - "set_tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa" + "set_tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa", + "stop": "{entity_name} meg\u00e1ll\u00edt\u00e1sa" }, "condition_type": { "is_closed": "{entity_name} z\u00e1rva van", diff --git a/homeassistant/components/cover/translations/id.json b/homeassistant/components/cover/translations/id.json index b38fcf86a17..d07f2f23ad2 100644 --- a/homeassistant/components/cover/translations/id.json +++ b/homeassistant/components/cover/translations/id.json @@ -1,9 +1,36 @@ { + "device_automation": { + "action_type": { + "close": "Tutup {entity_name}", + "close_tilt": "Tutup miring {entity_name}", + "open": "Buka {entity_name}", + "open_tilt": "Buka miring {entity_name}", + "set_position": "Tetapkan posisi {entity_name}", + "set_tilt_position": "Setel posisi miring {entity_name}", + "stop": "Hentikan {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} tertutup", + "is_closing": "{entity_name} menutup", + "is_open": "{entity_name} terbuka", + "is_opening": "{entity_name} membuka", + "is_position": "Posisi {entity_name} saat ini adalah", + "is_tilt_position": "Posisi miring {entity_name} saat ini adalah" + }, + "trigger_type": { + "closed": "{entity_name} tertutup", + "closing": "{entity_name} menutup", + "opened": "{entity_name} terbuka", + "opening": "{entity_name} membuka", + "position": "Perubahan posisi {entity_name}", + "tilt_position": "Perubahan posisi kemiringan {entity_name}" + } + }, "state": { "_": { "closed": "Tertutup", "closing": "Menutup", - "open": "Buka", + "open": "Terbuka", "opening": "Membuka", "stopped": "Terhenti" } diff --git a/homeassistant/components/cover/translations/ko.json b/homeassistant/components/cover/translations/ko.json index 0a666a8bd82..71a48bd532d 100644 --- a/homeassistant/components/cover/translations/ko.json +++ b/homeassistant/components/cover/translations/ko.json @@ -1,28 +1,29 @@ { "device_automation": { "action_type": { - "close": "{entity_name} \ub2eb\uae30", - "close_tilt": "{entity_name} \ub2eb\uae30", - "open": "{entity_name} \uc5f4\uae30", - "open_tilt": "{entity_name} \uc5f4\uae30", - "set_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \uc124\uc815\ud558\uae30", - "set_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \uc124\uc815\ud558\uae30" + "close": "{entity_name}\uc744(\ub97c) \ub2eb\uae30", + "close_tilt": "{entity_name}\uc744(\ub97c) \ub2eb\uae30", + "open": "{entity_name}\uc744(\ub97c) \uc5f4\uae30", + "open_tilt": "{entity_name}\uc744(\ub97c) \uc5f4\uae30", + "set_position": "{entity_name}\uc758 \uac1c\ud3d0 \uc704\uce58 \uc124\uc815\ud558\uae30", + "set_tilt_position": "{entity_name}\uc758 \uac1c\ud3d0 \uae30\uc6b8\uae30 \uc124\uc815\ud558\uae30", + "stop": "{entity_name}\uc744(\ub97c) \uc815\uc9c0\ud558\uae30" }, "condition_type": { - "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", - "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", - "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc774\uba74", - "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 ~ \uc774\uba74", - "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 ~ \uc774\uba74" + "is_closed": "{entity_name}\uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_closing": "{entity_name}\uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74", + "is_open": "{entity_name}\uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_opening": "{entity_name}\uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc774\uba74", + "is_position": "\ud604\uc7ac {entity_name}\uc758 \uac1c\ud3d0 \uc704\uce58\uac00 ~ \uc774\uba74", + "is_tilt_position": "\ud604\uc7ac {entity_name}\uc758 \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 ~ \uc774\uba74" }, "trigger_type": { - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", - "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc77c \ub54c", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", - "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc77c \ub54c", - "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 \ubcc0\ud560 \ub54c", - "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 \ubcc0\ud560 \ub54c" + "closed": "{entity_name}\uc774(\uac00) \ub2eb\ud614\uc744 \ub54c", + "closing": "{entity_name}\uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc77c \ub54c", + "opened": "{entity_name}\uc774(\uac00) \uc5f4\ub838\uc744 \ub54c", + "opening": "{entity_name}\uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc77c \ub54c", + "position": "{entity_name}\uc758 \uac1c\ud3d0 \uc704\uce58\uac00 \ubcc0\ud560 \ub54c", + "tilt_position": "{entity_name}\uc758 \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 \ubcc0\ud560 \ub54c" } }, "state": { diff --git a/homeassistant/components/cover/translations/zh-Hans.json b/homeassistant/components/cover/translations/zh-Hans.json index 04b25ad7cb8..765fcbeebe0 100644 --- a/homeassistant/components/cover/translations/zh-Hans.json +++ b/homeassistant/components/cover/translations/zh-Hans.json @@ -2,8 +2,11 @@ "device_automation": { "action_type": { "close": "\u5173\u95ed {entity_name}", + "close_tilt": "\u5173\u95ed {entity_name}", "open": "\u6253\u5f00 {entity_name}", + "open_tilt": "\u65cb\u5f00 {entity_name}", "set_position": "\u8bbe\u7f6e {entity_name} \u7684\u4f4d\u7f6e", + "set_tilt_position": "\u8bbe\u7f6e {entity_name} \u7684\u503e\u659c\u4f4d\u7f6e", "stop": "\u505c\u6b62 {entity_name}" }, "condition_type": { @@ -19,7 +22,8 @@ "closing": "{entity_name} \u6b63\u5728\u5173\u95ed", "opened": "{entity_name} \u5df2\u6253\u5f00", "opening": "{entity_name} \u6b63\u5728\u6253\u5f00", - "position": "{entity_name} \u7684\u4f4d\u7f6e\u53d8\u5316" + "position": "{entity_name} \u7684\u4f4d\u7f6e\u53d8\u5316", + "tilt_position": "{entity_name} \u7684\u503e\u659c\u4f4d\u7f6e\u53d8\u5316" } }, "state": { diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index df1a224bcce..01938344694 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -2,10 +2,9 @@ from cpuinfo import cpuinfo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, FREQUENCY_GIGAHERTZ import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" @@ -29,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([CpuSpeedSensor(name)], True) -class CpuSpeedSensor(Entity): +class CpuSpeedSensor(SensorEntity): """Representation of a CPU sensor.""" def __init__(self, name): @@ -54,7 +53,7 @@ class CpuSpeedSensor(Entity): return FREQUENCY_GIGAHERTZ @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.info is not None: attrs = { diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 72d2aa62ae0..6a3fc7b4215 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -5,11 +5,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -96,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class CupsSensor(Entity): +class CupsSensor(SensorEntity): """Representation of a CUPS sensor.""" def __init__(self, data, printer): @@ -131,7 +130,7 @@ class CupsSensor(Entity): return ICON_PRINTER @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._printer is None: return None @@ -155,7 +154,7 @@ class CupsSensor(Entity): self._available = self.data.available -class IPPSensor(Entity): +class IPPSensor(SensorEntity): """Implementation of the IPPSensor. This sensor represents the status of the printer. @@ -193,7 +192,7 @@ class IPPSensor(Entity): return PRINTER_STATES.get(key, key) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._attributes is None: return None @@ -232,7 +231,7 @@ class IPPSensor(Entity): self._available = self.data.available -class MarkerSensor(Entity): +class MarkerSensor(SensorEntity): """Implementation of the MarkerSensor. This sensor represents the percentage of ink or toner. @@ -271,7 +270,7 @@ class MarkerSensor(Entity): return PERCENTAGE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._attributes is None: return None diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index f2cb29515b0..f42534f509b 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_QUOTE, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = "http://apilayer.net/api/live" @@ -55,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class CurrencylayerSensor(Entity): +class CurrencylayerSensor(SensorEntity): """Implementing the Currencylayer sensor.""" def __init__(self, rest, base, quote): @@ -86,7 +85,7 @@ class CurrencylayerSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 16fd2b2ff56..092bbf8866d 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -16,16 +16,14 @@ 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, KEY_MAC, TIMEOUT +from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) -DOMAIN = "daikin" - PARALLEL_UPDATES = 0 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -COMPONENT_TYPES = ["climate", "sensor", "switch"] +PLATFORMS = ["climate", "sensor", "switch"] CONFIG_SCHEMA = vol.Schema( vol.All( @@ -83,9 +81,9 @@ 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 component in COMPONENT_TYPES: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -94,8 +92,8 @@ async def async_unload_entry(hass, config_entry): """Unload a config entry.""" await asyncio.wait( [ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in COMPONENT_TYPES + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index b9956a87af0..619f9c8d1d8 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -12,12 +12,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD -from .const import CONF_UUID, KEY_IP, KEY_MAC, TIMEOUT +from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) -@config_entries.HANDLERS.register("daikin") +@config_entries.HANDLERS.register(DOMAIN) class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" @@ -124,14 +124,6 @@ class FlowHandler(config_entries.ConfigFlow): return await self.async_step_user() return await self._create_device(host) - async def async_step_discovery(self, discovery_info): - """Initialize step from discovery.""" - _LOGGER.debug("Discovered device: %s", discovery_info) - await self.async_set_unique_id(discovery_info[KEY_MAC]) - self._abort_if_unique_id_configured() - self.host = discovery_info[KEY_IP] - return await self.async_step_user() - async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered Daikin device.""" _LOGGER.debug("Zeroconf user_input: %s", discovery_info) diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 00bbbefd051..5b4bdd28331 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -14,6 +14,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) +DOMAIN = "daikin" + ATTR_TARGET_TEMPERATURE = "target_temperature" ATTR_INSIDE_TEMPERATURE = "inside_temperature" ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 80de4ed34a5..a5b515ea918 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -1,4 +1,5 @@ """Support for Daikin AC sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ICON, @@ -6,7 +7,6 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, ) -from homeassistant.helpers.entity import Entity from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi from .const import ( @@ -49,7 +49,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities([DaikinSensor.factory(daikin_api, sensor) for sensor in sensors]) -class DaikinSensor(Entity): +class DaikinSensor(SensorEntity): """Representation of a Sensor.""" @staticmethod diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json index ef589eb7f6d..f1cb7eab8f6 100644 --- a/homeassistant/components/daikin/translations/hu.json +++ b/homeassistant/components/daikin/translations/hu.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "unknown": "V\u00e1ratlan hiba" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/id.json b/homeassistant/components/daikin/translations/id.json new file mode 100644 index 00000000000..8b7cfb5460e --- /dev/null +++ b/homeassistant/components/daikin/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "host": "Host", + "password": "Kata Sandi" + }, + "description": "Masukkan Alamat IP perangkat AC Daikin Anda. \n\nPerhatikan bahwa Kunci API dan Kata Sandi hanya digunakan untuk perangkat BRP072Cxx dan SKYFi.", + "title": "Konfigurasi AC Daikin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index e4cf54eb365..706a81b5f7f 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -16,7 +16,7 @@ "host": "Host", "password": "Wachtwoord" }, - "description": "Voer het IP-adres van uw Daikin AC in.", + "description": "Voer IP-adres van uw Daikin AC in.\n\nLet op dat API-sleutel en Wachtwoord alleen worden gebruikt door respectievelijk BRP072Cxx en SKYFi apparaten.", "title": "Daikin AC instellen" } } diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index b1dbf890eb9..18780c10310 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -13,7 +13,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DANFOSS_AIR_PLATFORMS = ["sensor", "binary_sensor", "switch"] +PLATFORMS = ["sensor", "binary_sensor", "switch"] DOMAIN = "danfoss_air" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) @@ -29,7 +29,7 @@ def setup(hass, config): hass.data[DOMAIN] = DanfossAir(conf[CONF_HOST]) - for platform in DANFOSS_AIR_PLATFORMS: + for platform in PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 251c9692021..792a95e8ac4 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -3,6 +3,7 @@ import logging from pydanfossair.commands import ReadCommand +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, @@ -10,7 +11,6 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from . import DOMAIN as DANFOSS_AIR_DOMAIN @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class DanfossAir(Entity): +class DanfossAir(SensorEntity): """Representation of a Sensor.""" def __init__(self, data, name, sensor_unit, sensor_type, device_class): diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 0bafdbb3d81..058969d96f9 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -6,7 +6,11 @@ import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol -from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + DEVICE_CLASS_TEMPERATURE, + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -29,7 +33,6 @@ from homeassistant.const import ( UV_INDEX, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -544,7 +547,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class DarkSkySensor(Entity): +class DarkSkySensor(SensorEntity): """Implementation of a Dark Sky sensor.""" def __init__( @@ -620,7 +623,7 @@ class DarkSkySensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -708,7 +711,7 @@ class DarkSkySensor(Entity): return state -class DarkSkyAlertSensor(Entity): +class DarkSkyAlertSensor(SensorEntity): """Implementation of a Dark Sky sensor.""" def __init__(self, forecast_data, sensor_type, name): @@ -739,7 +742,7 @@ class DarkSkyAlertSensor(Entity): return "mdi:alert-circle-outline" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._alerts diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index caa691b2369..98f08827c23 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -1,8 +1,9 @@ """The Remote Python Debugger integration.""" +from __future__ import annotations + from asyncio import Event import logging from threading import Thread -from typing import Optional import debugpy import voluptuous as vol @@ -40,7 +41,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = config[DOMAIN] async def debug_start( - call: Optional[ServiceCall] = None, *, wait: bool = True + call: ServiceCall | None = None, *, wait: bool = True ) -> None: """Start the debugger.""" debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 1b8c47d36a2..8b609fe3126 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -20,11 +20,6 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): - """Old way of setting up deCONZ integrations.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up a deCONZ bridge for a config entry. diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 616206949ed..99f559eec3d 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -92,7 +92,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): return DEVICE_CLASS.get(type(self._device)) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 44111fbbb1e..49f0cc4d149 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,5 +1,5 @@ """Support for deCONZ climate devices.""" -from typing import Optional +from __future__ import annotations from pydeconz.sensor import Thermostat @@ -195,7 +195,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): # Preset control @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return preset mode.""" return DECONZ_TO_PRESET_MODE.get(self._device.preset) @@ -244,7 +244,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): return TEMP_CELSIUS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the thermostat.""" attr = {} diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 67effa4e81b..5ed1def66c2 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -28,7 +28,7 @@ CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" CONF_ALLOW_NEW_DEVICES = "allow_new_devices" CONF_MASTER_GATEWAY = "master" -SUPPORTED_PLATFORMS = [ +PLATFORMS = [ BINARY_SENSOR_DOMAIN, CLIMATE_DOMAIN, COVER_DOMAIN, @@ -60,7 +60,7 @@ COVER_TYPES = DAMPERS + WINDOW_COVERS FANS = ["Fan"] # Locks -LOCKS = ["Door Lock"] +LOCKS = ["Door Lock", "ZHADoorLock"] LOCK_TYPES = LOCKS # Switches diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 6e57d08302a..301d1753591 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -2,7 +2,8 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DEVICE_CLASS_WINDOW, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_SHADE, DOMAIN, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, @@ -80,9 +81,9 @@ class DeconzCover(DeconzDevice, CoverEntity): def device_class(self): """Return the class of the cover.""" if self._device.type in DAMPERS: - return "damper" + return DEVICE_CLASS_DAMPER if self._device.type in WINDOW_COVERS: - return DEVICE_CLASS_WINDOW + return DEVICE_CLASS_SHADE @property def current_cover_position(self): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 81d3aa94d31..706850477d8 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -106,8 +106,11 @@ class DeconzEvent(DeconzBase): self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) - async def async_update_device_registry(self): + async def async_update_device_registry(self) -> None: """Update device registry.""" + if not self.device_info: + return + device_registry = ( await self.gateway.hass.helpers.device_registry.async_get_registry() ) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 5ee0a00f04f..e8e43d384b1 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from . import DOMAIN -from .const import LOGGER from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE CONF_SUBTYPE = "subtype" @@ -414,16 +413,13 @@ async def async_validate_trigger_config(hass, config): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if ( - not device - or device.model not in REMOTES - or trigger not in REMOTES[device.model] - ): - if not device: - raise InvalidDeviceAutomationConfig( - f"deCONZ trigger {trigger} device with id " - f"{config[CONF_DEVICE_ID]} not found" - ) + if not device: + raise InvalidDeviceAutomationConfig( + f"deCONZ trigger {trigger} device with ID " + f"{config[CONF_DEVICE_ID]} not found" + ) + + if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig( f"deCONZ trigger {trigger} is not valid for device " f"{device} ({config[CONF_DEVICE_ID]})" @@ -443,8 +439,9 @@ async def async_attach_trigger(hass, config, action, automation_info): deconz_event = _get_deconz_event_from_device_id(hass, device.id) if deconz_event is None: - LOGGER.error("No deconz_event tied to device %s found", device.name) - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig( + f'No deconz_event tied to device "{device.name}" found' + ) event_id = deconz_event.serial diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 1ca4c8ff9c2..aca92f893c7 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,4 +1,6 @@ -"""Support for deCONZ switches.""" +"""Support for deCONZ fans.""" +from __future__ import annotations + from homeassistant.components.fan import ( DOMAIN, SPEED_HIGH, @@ -10,25 +12,19 @@ from homeassistant.components.fan import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from .const import FANS, NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -SPEEDS = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 2, SPEED_HIGH: 4} -SUPPORTED_ON_SPEEDS = {1: SPEED_LOW, 2: SPEED_MEDIUM, 4: SPEED_HIGH} +ORDERED_NAMED_FAN_SPEEDS = [1, 2, 3, 4] - -def convert_speed(speed: int) -> str: - """Convert speed from deCONZ to HASS. - - Fallback to medium speed if unsupported by HASS fan platform. - """ - if speed in SPEEDS.values(): - for hass_speed, deconz_speed in SPEEDS.items(): - if speed == deconz_speed: - return hass_speed - return SPEED_MEDIUM +LEGACY_SPEED_TO_DECONZ = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 2, SPEED_HIGH: 4} +LEGACY_DECONZ_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 4: SPEED_HIGH} async def async_setup_entry(hass, config_entry, async_add_entities) -> None: @@ -67,8 +63,8 @@ class DeconzFan(DeconzDevice, FanEntity): """Set up fan.""" super().__init__(device, gateway) - self._default_on_speed = SPEEDS[SPEED_MEDIUM] - if self.speed != SPEED_OFF: + self._default_on_speed = 2 + if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed self._features = SUPPORT_SET_SPEED @@ -76,17 +72,58 @@ class DeconzFan(DeconzDevice, FanEntity): @property def is_on(self) -> bool: """Return true if fan is on.""" - return self.speed != SPEED_OFF + return self._device.speed != 0 @property - def speed(self) -> int: - """Return the current speed.""" - return convert_speed(self._device.speed) + def percentage(self) -> int | None: + """Return the current speed percentage.""" + if self._device.speed == 0: + return 0 + if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS: + return None + return ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, self._device.speed + ) + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return len(ORDERED_NAMED_FAN_SPEEDS) @property def speed_list(self) -> list: - """Get the list of available speeds.""" - return list(SPEEDS) + """Get the list of available speeds. + + Legacy fan support. + """ + return list(LEGACY_SPEED_TO_DECONZ) + + def speed_to_percentage(self, speed: str) -> int: + """Convert speed to percentage. + + Legacy fan support. + """ + if speed == SPEED_OFF: + return 0 + + if speed not in LEGACY_SPEED_TO_DECONZ: + speed = SPEED_MEDIUM + + return ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, LEGACY_SPEED_TO_DECONZ[speed] + ) + + def percentage_to_speed(self, percentage: int) -> str: + """Convert percentage to speed. + + Legacy fan support. + """ + if percentage == 0: + return SPEED_OFF + return LEGACY_DECONZ_TO_SPEED.get( + percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage), + SPEED_MEDIUM, + ) @property def supported_features(self) -> int: @@ -96,24 +133,26 @@ class DeconzFan(DeconzDevice, FanEntity): @callback def async_update_callback(self, force_update=False) -> None: """Store latest configured speed from the device.""" - if self.speed != SPEED_OFF and self._device.speed != self._default_on_speed: + if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed super().async_update_callback(force_update) + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + await self._device.set_speed( + percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) + ) + async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed not in SPEEDS: + """Set the speed of the fan. + + Legacy fan support. + """ + if speed not in LEGACY_SPEED_TO_DECONZ: raise ValueError(f"Unsupported speed {speed}") - await self._device.set_speed(SPEEDS[speed]) + await self._device.set_speed(LEGACY_SPEED_TO_DECONZ[speed]) - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str = None, @@ -122,10 +161,15 @@ class DeconzFan(DeconzDevice, FanEntity): **kwargs, ) -> None: """Turn on fan.""" - if not speed: - speed = convert_speed(self._default_on_speed) - await self.async_set_speed(speed) + new_speed = self._default_on_speed + + if percentage is not None: + new_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + + await self._device.set_speed(new_speed) async def async_turn_off(self, **kwargs) -> None: """Turn off fan.""" - await self.async_set_speed(SPEED_OFF) + await self._device.set_speed(0) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index a6cbb2acef9..2b38f6956be 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -26,7 +26,7 @@ from .const import ( NEW_LIGHT, NEW_SCENE, NEW_SENSOR, - SUPPORTED_PLATFORMS, + PLATFORMS, ) from .deconz_event import async_setup_events, async_unload_events from .errors import AuthenticationRequired, CannotConnect @@ -184,10 +184,10 @@ class DeconzGateway: ) return False - for component in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( - self.config_entry, component + self.config_entry, platform ) ) @@ -259,9 +259,9 @@ class DeconzGateway: self.api.async_connection_status_callback = None self.api.close() - for component in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, component + self.config_entry, platform ) for unsub_dispatcher in self.listeners: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 2da435c5530..f7ae45781ac 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -223,7 +223,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): await self._device.async_set_state(data) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return {"is_deconz_group": self._device.type == "LightGroup"} @@ -275,9 +275,9 @@ class DeconzGroup(DeconzBaseLight): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - attributes = dict(super().device_state_attributes) + attributes = dict(super().extra_state_attributes) attributes["all_on"] = self._device.all_on return attributes diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 4d428af3673..4b6da1e0b97 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 +from .const import LOCKS, NEW_LIGHT, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_lock(lights=gateway.api.lights.values()): + def async_add_lock_from_light(lights=gateway.api.lights.values()): """Add lock from deCONZ.""" entities = [] @@ -28,11 +28,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light ) ) - async_add_lock() + @callback + def async_add_lock_from_sensor(sensors=gateway.api.sensors.values()): + """Add lock from deCONZ.""" + entities = [] + + for sensor in sensors: + + if sensor.type in LOCKS and sensor.uniqueid not in gateway.entities[DOMAIN]: + entities.append(DeconzLock(sensor, gateway)) + + if entities: + async_add_entities(entities) + + gateway.listeners.append( + async_dispatcher_connect( + hass, + gateway.async_signal_new_device(NEW_SENSOR), + async_add_lock_from_sensor, + ) + ) + + async_add_lock_from_light() + async_add_lock_from_sensor() class DeconzLock(DeconzDevice, LockEntity): diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 85982244364..e6f3e9362cd 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -1,6 +1,7 @@ """Describe deCONZ logbook events.""" +from __future__ import annotations -from typing import Callable, Optional +from typing import Callable from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT from homeassistant.core import HomeAssistant, callback @@ -125,7 +126,7 @@ def async_describe_events( @callback def async_describe_deconz_event(event: Event) -> dict: """Describe deCONZ logbook event.""" - deconz_event: Optional[DeconzEvent] = _get_deconz_event_from_device_id( + deconz_event: DeconzEvent | None = _get_deconz_event_from_device_id( hass, event.data[ATTR_DEVICE_ID] ) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 22711b84b9d..5cce8858910 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==77"], + "requirements": ["pydeconz==78"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 9d71fd0a9f9..a38b7cb20aa 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,6 +3,7 @@ from pydeconz.sensor import ( Battery, Consumption, Daylight, + DoorLock, Humidity, LightLevel, Power, @@ -12,7 +13,7 @@ from pydeconz.sensor import ( Thermostat, ) -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, @@ -103,7 +104,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if ( not sensor.BINARY and sensor.type - not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE + not in Battery.ZHATYPE + + DoorLock.ZHATYPE + + Switch.ZHATYPE + + Thermostat.ZHATYPE and sensor.uniqueid not in gateway.entities[DOMAIN] ): entities.append(DeconzSensor(sensor, gateway)) @@ -122,7 +126,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class DeconzSensor(DeconzDevice): +class DeconzSensor(DeconzDevice, SensorEntity): """Representation of a deCONZ sensor.""" TYPE = DOMAIN @@ -155,7 +159,7 @@ class DeconzSensor(DeconzDevice): return UNIT_OF_MEASUREMENT.get(type(self._device)) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} @@ -186,7 +190,7 @@ class DeconzSensor(DeconzDevice): return attr -class DeconzBattery(DeconzDevice): +class DeconzBattery(DeconzDevice, SensorEntity): """Battery class for when a device is only represented as an event.""" TYPE = DOMAIN @@ -200,7 +204,18 @@ class DeconzBattery(DeconzDevice): @property def unique_id(self): - """Return a unique identifier for this device.""" + """Return a unique identifier for this device. + + Normally there should only be one battery sensor per device from deCONZ. + With specific Danfoss devices each endpoint can report its own battery state. + """ + if self._device.manufacturer == "Danfoss" and self._device.modelid in [ + "0x8030", + "0x8031", + "0x8034", + "0x8035", + ]: + return f"{super().unique_id}-battery" return f"{self.serial}-battery" @property @@ -224,7 +239,7 @@ class DeconzBattery(DeconzDevice): return PERCENTAGE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the battery.""" attr = {} diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index 1703037fc64..3bce097f7d3 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -1,32 +1,66 @@ configure: - description: Set attribute of device in deCONZ. See https://home-assistant.io/integrations/deconz/#device-services for details. + name: Configure + description: >- + Configure attributes of either a device endpoint in deCONZ + or the deCONZ service itself. fields: entity: - description: Entity id representing a specific device in deCONZ. + name: Entity + description: Represents a specific device endpoint in deCONZ. example: "light.rgb_light" + selector: + entity: + integration: deconz field: + name: Path + selector: + text: description: >- - Field is a string representing a full path to deCONZ endpoint (when + String representing a full path to deCONZ endpoint (when entity is not specified) or a subpath of the device path for the entity (when entity is specified). example: '"/lights/1/state" or "/state"' data: - description: Data is a JSON object with what data you want to alter. + name: Configuration payload + required: true + selector: + object: + description: JSON object with what data you want to alter. example: '{"on": true}' bridgeid: - description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + name: Bridge identifier + selector: + text: + description: >- + Unique string for each deCONZ hardware. + It can be found as part of the integration name. + Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" device_refresh: - description: Refresh device lists from deCONZ. + name: Device refresh + description: Refresh available devices from deCONZ. fields: bridgeid: - description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + name: Bridge identifier + selector: + text: + description: >- + Unique string for each deCONZ hardware. + It can be found as part of the integration name. + Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" remove_orphaned_entries: + name: Remove orphaned entries description: Clean up device and entity registry entries orphaned by deCONZ. fields: bridgeid: - description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + name: Bridge identifier + selector: + text: + description: >- + Unique string for each deCONZ hardware. + It can be found as part of the integration name. + Useful if you run multiple deCONZ integrations. example: "00212EFFFF012345" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index bbda4e8c0cb..258de620a54 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -18,8 +18,8 @@ "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" }, "hassio_confirm": { - "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 Hass.io add-on {addon}?" + "title": "deCONZ Zigbee gateway via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the add-on {addon}?" } }, "error": { diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index 0aef2e3ec98..24e36ecbe55 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -13,8 +13,8 @@ "flow_title": "deCONZ Zigbee \u0448\u043b\u044e\u0437 ({host})", "step": { "hassio_confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 hass.io {addon}?", - "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 Supervisor {addon}?", + "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430" }, "link": { "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 49374ee123f..5957dc88c03 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 (complement de Hass.io)" + "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" }, "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\"", diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 52cbd607b7f..7e08a89ec31 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 hass.io {addon}?", - "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + "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" }, "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/da.json b/homeassistant/components/deconz/translations/da.json index 50cdd242ad0..be165a206bf 100644 --- a/homeassistant/components/deconz/translations/da.json +++ b/homeassistant/components/deconz/translations/da.json @@ -13,8 +13,8 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Hass.io-tilf\u00f8jelsen {addon}?", - "title": "deCONZ Zigbee-gateway via Hass.io-tilf\u00f8jelse" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Supervisor-tilf\u00f8jelsen {addon}?", + "title": "deCONZ Zigbee-gateway via Supervisor-tilf\u00f8jelse" }, "link": { "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 75b807b8848..a8575d212d6 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?", - "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on" + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?", + "title": "deCONZ Zigbee Gateway \u00fcber das Supervisor Add-on" }, "link": { "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json index e454d080ad7..e439d1da949 100644 --- a/homeassistant/components/deconz/translations/es-419.json +++ b/homeassistant/components/deconz/translations/es-419.json @@ -13,8 +13,8 @@ "flow_title": "Puerta de enlace Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", - "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io" + "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento Supervisor {addon}?", + "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Supervisor" }, "link": { "description": "Desbloquee su puerta de enlace deCONZ para registrarse con Home Assistant. \n\n 1. Vaya a Configuraci\u00f3n deCONZ - > Gateway - > Avanzado \n 2. Presione el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 62ab509e268..b237d84fafc 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -14,8 +14,8 @@ "flow_title": "pasarela deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de hass.io?", - "title": "Add-on deCONZ Zigbee v\u00eda Hass.io" + "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de Supervisor?", + "title": "Add-on deCONZ Zigbee v\u00eda Supervisor" }, "link": { "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"", diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index 9f6644ea186..b949208a664 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -15,7 +15,7 @@ "step": { "hassio_confirm": { "description": "Kas soovid seadistada Home Assistant-i \u00fchenduse deCONZ-l\u00fc\u00fcsiga, mida pakub Hass.io lisandmoodul {addon} ?", - "title": "deCONZ Zigbee v\u00e4rav Hass.io pistikprogrammi kaudu" + "title": "deCONZ Zigbee l\u00fc\u00fcs Hass.io 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\"", diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index b64819471a0..d24b592ac10 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -14,8 +14,8 @@ "flow_title": "Passerelle deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?", - "title": "Passerelle deCONZ Zigbee via l'add-on Hass.io" + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par le module compl\u00e9mentaire Hass.io {addon} ?", + "title": "Passerelle deCONZ Zigbee via le module compl\u00e9mentaire Hass.io" }, "link": { "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer avec Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres avanc\u00e9s du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 4651a7c08e0..61322087cbf 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -2,8 +2,9 @@ "config": { "abort": { "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "Az \u00e1tj\u00e1r\u00f3 konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", + "no_hardware_available": "Nincs deCONZ-hoz csatlakoztatott r\u00e1di\u00f3hardver", "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3", "updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve" }, @@ -13,7 +14,7 @@ "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})", "step": { "hassio_confirm": { - "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" + "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Supervisor kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "link": { "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", @@ -51,30 +52,30 @@ }, "trigger_type": { "remote_awakened": "A k\u00e9sz\u00fcl\u00e9k fel\u00e9bredt", - "remote_button_double_press": "\" {subtype} \" gombra k\u00e9tszer kattintottak", - "remote_button_long_press": "A \" {subtype} \" gomb folyamatosan lenyomva", - "remote_button_long_release": "A \" {subtype} \" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", - "remote_button_quadruple_press": "\" {subtype} \" gombra n\u00e9gyszer kattintottak", - "remote_button_quintuple_press": "\" {subtype} \" gombra \u00f6tsz\u00f6r kattintottak", - "remote_button_rotated": "A gomb elforgatva: \" {subtype} \"", - "remote_button_rotation_stopped": "A (z) \" {subtype} \" gomb forg\u00e1sa le\u00e1llt", - "remote_button_short_press": "\" {subtype} \" gomb lenyomva", - "remote_button_short_release": "\"{alt\u00edpus}\" gomb elengedve", - "remote_button_triple_press": "\" {subtype} \" gombra h\u00e1romszor kattintottak", - "remote_double_tap": "Az \" {subtype} \" eszk\u00f6z dupla kattint\u00e1sa", + "remote_button_double_press": "\"{subtype}\" gombra k\u00e9tszer kattintottak", + "remote_button_long_press": "A \"{subtype}\" gomb folyamatosan lenyomva", + "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", + "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", + "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"", + "remote_button_rotation_stopped": "A (z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", + "remote_button_short_press": "\"{subtype}\" gomb lenyomva", + "remote_button_short_release": "\"{subtype}\" gomb elengedve", + "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak", + "remote_double_tap": "Az \"{subtype}\" eszk\u00f6z dupla kattint\u00e1sa", "remote_double_tap_any_side": "A k\u00e9sz\u00fcl\u00e9k b\u00e1rmelyik oldal\u00e1n dupl\u00e1n koppint.", "remote_falling": "K\u00e9sz\u00fcl\u00e9k szabades\u00e9sben", "remote_flip_180_degrees": "180 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", "remote_gyro_activated": "A k\u00e9sz\u00fcl\u00e9k meg lett r\u00e1zva", - "remote_moved": "Az eszk\u00f6z a \" {subtype} \"-lal felfel\u00e9 mozgatva", + "remote_moved": "Az eszk\u00f6z a \"{subtype}\"-lal felfel\u00e9 mozgatva", "remote_moved_any_side": "A k\u00e9sz\u00fcl\u00e9k valamelyik oldal\u00e1val felfel\u00e9 mozogott", - "remote_rotate_from_side_1": "Az eszk\u00f6z a \"1. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", - "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", - "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", - "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", - "remote_rotate_from_side_5": "Az eszk\u00f6z a \"5. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", - "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_1": "Az eszk\u00f6z az \"1. oldalr\u00f3l\" a \"{subtype}\"-ra fordult", + "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \"{subtype}\"-ra fordult", + "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \"{subtype}\"-ra fordult", + "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \"{subtype}\"-ra fordult", + "remote_rotate_from_side_5": "Az eszk\u00f6z az \"5. oldalr\u00f3l\" a \"{subtype}\"-ra fordult", + "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \"{subtype}\"-ra fordult", "remote_turned_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val megegyez\u0151en fordult", "remote_turned_counter_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val ellent\u00e9tes ir\u00e1nyban fordult" } diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index 0d46cf7c176..d7fb26f8d52 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -2,15 +2,103 @@ "config": { "abort": { "already_configured": "Bridge sudah dikonfigurasi", - "no_bridges": "deCONZ bridges tidak ditemukan" + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_bridges": "deCONZ bridge tidak ditemukan", + "no_hardware_available": "Tidak ada perangkat keras radio yang terhubung ke deCONZ", + "not_deconz_bridge": "Bukan bridge deCONZ", + "updated_instance": "Instans deCONZ yang diperbarui dengan alamat host baru" }, "error": { "no_key": "Tidak bisa mendapatkan kunci API" }, + "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { + "hassio_confirm": { + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke gateway deCONZ yang disediakan oleh add-on Supervisor {addon}?", + "title": "Gateway Zigbee deCONZ melalui add-on Supervisor" + }, "link": { - "description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"", - "title": "Tautan dengan deCONZ" + "description": "Buka gateway deCONZ Anda untuk mendaftarkan ke Home Assistant. \n\n1. Buka pengaturan sistem deCONZ \n2. Tekan tombol \"Authenticate app\"", + "title": "Tautkan dengan deCONZ" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Port" + } + }, + "user": { + "data": { + "host": "Pilih gateway deCONZ yang ditemukan" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Kedua tombol", + "bottom_buttons": "Tombol bawah", + "button_1": "Tombol pertama", + "button_2": "Tombol kedua", + "button_3": "Tombol ketiga", + "button_4": "Tombol keempat", + "close": "Tutup", + "dim_down": "Redupkan", + "dim_up": "Terangkan", + "left": "Kiri", + "open": "Buka", + "right": "Kanan", + "side_1": "Sisi 1", + "side_2": "Sisi 2", + "side_3": "Sisi 3", + "side_4": "Sisi 4", + "side_5": "Sisi 5", + "side_6": "Sisi 6", + "top_buttons": "Tombol atas", + "turn_off": "Matikan", + "turn_on": "Nyalakan" + }, + "trigger_type": { + "remote_awakened": "Perangkat terbangun", + "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali", + "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan", + "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali", + "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali", + "remote_button_rotated": "Tombol diputar \"{subtype}\"", + "remote_button_rotated_fast": "Tombol diputar cepat \"{subtype}\"", + "remote_button_rotation_stopped": "Pemutaran tombol \"{subtype}\" berhenti", + "remote_button_short_press": "Tombol \"{subtype}\" ditekan", + "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", + "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali", + "remote_double_tap": "Perangkat \"{subtype}\" diketuk dua kali", + "remote_double_tap_any_side": "Perangkat diketuk dua kali di sisi mana pun", + "remote_falling": "Perangkat jatuh bebas", + "remote_flip_180_degrees": "Perangkat dibalik 180 derajat", + "remote_flip_90_degrees": "Perangkat dibalik 90 derajat", + "remote_gyro_activated": "Perangkat diguncangkan", + "remote_moved": "Perangkat dipindahkan dengan \"{subtype}\" ke atas", + "remote_moved_any_side": "Perangkat dipindahkan dengan sisi mana pun menghadap ke atas", + "remote_rotate_from_side_1": "Perangkat diputar dari \"sisi 1\" ke \"{subtype}\"", + "remote_rotate_from_side_2": "Perangkat diputar dari \"sisi 2\" ke \"{subtype}\"", + "remote_rotate_from_side_3": "Perangkat diputar dari \"sisi 3\" ke \"{subtype}\"", + "remote_rotate_from_side_4": "Perangkat diputar dari \"sisi 4\" ke \"{subtype}\"", + "remote_rotate_from_side_5": "Perangkat diputar dari \"sisi 5\" ke \"{subtype}\"", + "remote_rotate_from_side_6": "Perangkat diputar dari \"sisi 6\" ke \"{subtype}\"", + "remote_turned_clockwise": "Perangkat diputar searah jarum jam", + "remote_turned_counter_clockwise": "Perangkat diputar berlawanan arah jarum jam" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "Izinkan sensor CLIP deCONZ", + "allow_deconz_groups": "Izinkan grup lampu deCONZ", + "allow_new_devices": "Izinkan penambahan otomatis perangkat baru" + }, + "description": "Konfigurasikan visibilitas jenis perangkat deCONZ", + "title": "Opsi deCONZ" } } } diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 00ecd316da7..fd81ebad8cf 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -15,7 +15,7 @@ "step": { "hassio_confirm": { "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Gateway Pigmee deCONZ tramite il componente aggiuntivo di Hass.io" + "title": "Gateway deCONZ Zigbee tramite il componente aggiuntivo di Hass.io" }, "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\"", diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index bd8aef75dd6..811b2400ddd 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -4,6 +4,7 @@ "already_configured": "\ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_hardware_available": "deCONZ\uc5d0 \uc5f0\uacb0\ub41c \ubb34\uc120 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", "not_deconz_bridge": "deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", "updated_instance": "deCONZ \uc778\uc2a4\ud134\uc2a4\ub97c \uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4" }, @@ -13,11 +14,11 @@ "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?", + "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" }, "link": { - "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30.\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Authenticate app\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", + "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", "title": "deCONZ \uc5f0\uacb0\ud558\uae30" }, "manual_input": { @@ -58,33 +59,34 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub420 \ub54c", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", - "remote_button_rotated": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\ub420 \ub54c", - "remote_button_rotation_stopped": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\uc744 \uba48\ucd9c \ub54c", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c", - "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14 \ud0ed \ub420 \ub54c", - "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \ub354\ube14 \ud0ed \ub420 \ub54c", + "remote_awakened": "\uae30\uae30\uc758 \uc808\uc804 \ubaa8\ub4dc\uac00 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub838\uc744 \ub54c", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_button_rotated": "\"{subtype}\"(\uc73c)\ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_button_rotated_fast": "\"{subtype}\"(\uc73c)\ub85c \ubc84\ud2bc\uc774 \ube60\ub974\uac8c \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_button_rotation_stopped": "\"{subtype}\"(\uc73c)\ub85c \ubc84\ud2bc\ud68c\uc804\uc774 \uba48\ucd94\uc5c8\uc744 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\"\uc774(\uac00) \ub354\ube14 \ud0ed \ub418\uc5c8\uc744 \ub54c", + "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \ub354\ube14 \ud0ed \ub418\uc5c8\uc744 \ub54c", "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9c8 \ub54c", - "remote_flip_180_degrees": "\uae30\uae30\uac00 180\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", - "remote_flip_90_degrees": "\uae30\uae30\uac00 90\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", - "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", - "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c", - "remote_moved_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c", - "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_turned_clockwise": "\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_turned_counter_clockwise": "\ubc18\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c" + "remote_flip_180_degrees": "\uae30\uae30\uac00 180\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc84c\uc744 \ub54c", + "remote_flip_90_degrees": "\uae30\uae30\uac00 90\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc84c\uc744 \ub54c", + "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub838\uc744 \ub54c", + "remote_moved": "\uae30\uae30\uc758 \"{subtype}\"\uc774(\uac00) \uc704\ub85c \ud5a5\ud55c \ucc44\ub85c \uc6c0\uc9c1\uc600\uc744 \ub54c", + "remote_moved_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc600\uc744 \ub54c", + "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_turned_clockwise": "\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "remote_turned_counter_clockwise": "\ubc18\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c" } }, "options": { @@ -92,7 +94,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", - "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" + "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9", + "allow_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30\uc758 \uc790\ub3d9 \ucd94\uac00 \ud5c8\uc6a9\ud558\uae30" }, "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131", "title": "deCONZ \uc635\uc158" diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json index bb556842194..06b8dbacdc5 100644 --- a/homeassistant/components/deconz/translations/lb.json +++ b/homeassistant/components/deconz/translations/lb.json @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mat der deCONZ gateway ze verbannen d\u00e9i vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", - "title": "deCONZ Zigbee gateway via Hass.io add-on" + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mat der deCONZ gateway ze verbannen d\u00e9i vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?", + "title": "deCONZ Zigbee gateway via Supervisor add-on" }, "link": { "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 2d43ca63bfe..833050eaf92 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Bridge is al geconfigureerd", - "already_in_progress": "Configuratiestroom voor bridge wordt al ingesteld.", + "already_in_progress": "De configuratiestroom is al aan de gang", "no_bridges": "Geen deCONZ apparaten ontdekt", "no_hardware_available": "Geen radiohardware aangesloten op deCONZ", "not_deconz_bridge": "Dit is geen deCONZ bridge", @@ -14,8 +14,8 @@ "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { - "description": "Wilt u de Home Assistant configureren om verbinding te maken met de deCONZ gateway van de hass.io add-on {addon}?", - "title": "deCONZ Zigbee Gateway via Hass.io add-on" + "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" }, "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\"", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index c1435dbb186..4dcd693b5f4 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -14,7 +14,7 @@ "flow_title": "", "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegg {addon} ?", + "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" }, "link": { diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index a13c94d82d7..450fa7707d1 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -12,8 +12,8 @@ }, "step": { "hassio_confirm": { - "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on hass.io {addon} ?", - "title": "Gateway deCONZ Zigbee via add-on Hass.io" + "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on Supervisor {addon} ?", + "title": "Gateway deCONZ Zigbee via add-on Supervisor" }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index 725ce07a1b6..cc8b4ab19f2 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -11,8 +11,8 @@ }, "step": { "hassio_confirm": { - "description": "Deseja configurar o Home Assistant para se conectar ao gateway deCONZ fornecido pelo addon Hass.io {addon} ?", - "title": "Gateway Zigbee deCONZ via addon Hass.io" + "description": "Deseja configurar o Home Assistant para se conectar ao gateway deCONZ fornecido pelo addon Supervisor {addon} ?", + "title": "Gateway Zigbee deCONZ via addon Supervisor" }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", diff --git a/homeassistant/components/deconz/translations/sl.json b/homeassistant/components/deconz/translations/sl.json index cf5600c20e4..9e8ed42c07e 100644 --- a/homeassistant/components/deconz/translations/sl.json +++ b/homeassistant/components/deconz/translations/sl.json @@ -13,8 +13,8 @@ "flow_title": "deCONZ Zigbee prehod ({host})", "step": { "hassio_confirm": { - "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Hass.io {addon} ?", - "title": "deCONZ Zigbee prehod preko dodatka Hass.io" + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Supervisor {addon} ?", + "title": "deCONZ Zigbee prehod preko dodatka Supervisor" }, "link": { "description": "Odklenite va\u0161 deCONZ gateway za registracijo s Home Assistant-om. \n1. Pojdite v deCONZ sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 4d709a43af1..c9814734af0 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -13,8 +13,8 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Hass.io-till\u00e4gget {addon}?", - "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg" + "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Supervisor-till\u00e4gget {addon}?", + "title": "deCONZ Zigbee gateway via Supervisor till\u00e4gg" }, "link": { "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen", diff --git a/homeassistant/components/deconz/translations/uk.json b/homeassistant/components/deconz/translations/uk.json index b5de362a731..3b09a517385 100644 --- a/homeassistant/components/deconz/translations/uk.json +++ b/homeassistant/components/deconz/translations/uk.json @@ -14,8 +14,8 @@ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", + "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" }, "link": { "description": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 \u0432 Home Assistant: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0438 deCONZ - > Gateway - > Advanced.\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", diff --git a/homeassistant/components/deconz/translations/zh-Hans.json b/homeassistant/components/deconz/translations/zh-Hans.json index a85ed6d72ca..dfe8209fa1c 100644 --- a/homeassistant/components/deconz/translations/zh-Hans.json +++ b/homeassistant/components/deconz/translations/zh-Hans.json @@ -21,11 +21,11 @@ "side_3": "\u7b2c 3 \u9762", "side_4": "\u7b2c 4 \u9762", "side_5": "\u7b2c 5 \u9762", - "side_6": "\u7b2c 6 \u9762", - "turn_off": "\u5173\u95ed" + "side_6": "\u7b2c 6 \u9762" }, "trigger_type": { "remote_awakened": "\u8bbe\u5907\u5524\u9192", + "remote_button_rotation_stopped": "\u6309\u94ae \"{subtype}\" \u505c\u6b62\u65cb\u8f6c", "remote_double_tap": "\u8bbe\u5907\u7684\u201c{subtype}\u201d\u88ab\u8f7b\u6572\u4e24\u6b21", "remote_falling": "\u8bbe\u5907\u81ea\u7531\u843d\u4f53", "remote_gyro_activated": "\u8bbe\u5907\u6447\u6643", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index 335aa73a67c..c17d2038127 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\u6574\u5408 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", - "title": "\u900f\u904e Hass.io \u9644\u52a0\u7d44\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" + "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" }, "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", diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 2fc436eda20..45c42c4bb1c 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -4,10 +4,8 @@ from functools import wraps import logging import time -from bluepy.btle import ( # pylint: disable=import-error, no-member, no-name-in-module - BTLEException, -) -import decora # pylint: disable=import-error, no-member +from bluepy.btle import BTLEException # pylint: disable=import-error +import decora # pylint: disable=import-error import voluptuous as vol from homeassistant.components.light import ( @@ -64,7 +62,7 @@ def retry(method): return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( - "Decora connect error for device %s. Reconnecting...", + "Decora connect error for device %s. Reconnecting", device.name, ) # pylint: disable=protected-access diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 0058816d318..cff93c89954 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -5,11 +5,10 @@ from pydelijn.api import Passages from pydelijn.common import HttpException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -58,7 +57,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, True) -class DeLijnPublicTransportSensor(Entity): +class DeLijnPublicTransportSensor(SensorEntity): """Representation of a Ruter sensor.""" def __init__(self, line): @@ -126,6 +125,6 @@ class DeLijnPublicTransportSensor(Entity): return "mdi:bus" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return attributes for the sensor.""" return self._attributes diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 5e8df89c20d..0c79e6f835e 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -4,7 +4,7 @@ import logging from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -17,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None @@ -68,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class DelugeSensor(Entity): +class DelugeSensor(SensorEntity): """Representation of a Deluge sensor.""" def __init__(self, sensor_type, deluge_client, client_name): diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 09c3d27a1bc..b32537ae44e 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -50,9 +50,9 @@ async def async_setup(hass, config): ) # Set up demo platforms - for component in COMPONENTS_WITH_DEMO_PLATFORM: + for platform in COMPONENTS_WITH_DEMO_PLATFORM: hass.async_create_task( - hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) + hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config) ) config.setdefault(ha.DOMAIN, {}) @@ -146,9 +146,9 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set the config entry up.""" # Set up demo platforms with config entry - for component in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: + for platform in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index 35f72e9e0cd..f99693bfeb2 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -5,7 +5,6 @@ from homeassistant import config_entries from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -# pylint: disable=unused-import from . import DOMAIN CONF_STRING = "string" diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index c79b53c0918..c406ffdb214 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,5 +1,5 @@ """Demo fan platform that has a fake fan.""" -from typing import List, Optional +from __future__ import annotations from homeassistant.components.fan import ( SPEED_HIGH, @@ -110,8 +110,8 @@ class BaseDemoFan(FanEntity): unique_id: str, name: str, supported_features: int, - preset_modes: Optional[List[str]], - speed_list: Optional[List[str]], + preset_modes: list[str] | None, + speed_list: list[str] | None, ) -> None: """Initialize the entity.""" self.hass = hass @@ -211,7 +211,7 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): """A demonstration fan component that uses percentages.""" @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed.""" return self._percentage @@ -227,12 +227,12 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): self.schedule_update_ha_state() @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., auto, smart, interval, favorite.""" return self._preset_mode @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return self._preset_modes @@ -271,7 +271,7 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): """An async demonstration fan component that uses percentages.""" @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed.""" return self._percentage @@ -287,12 +287,12 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): self.async_write_ha_state() @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., auto, smart, interval, favorite.""" return self._preset_mode @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return self._preset_modes diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index 76dea52e846..f288a2fd340 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -1,9 +1,10 @@ """Demo platform for the geolocation component.""" +from __future__ import annotations + from datetime import timedelta import logging from math import cos, pi, radians, sin import random -from typing import Optional from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import LENGTH_KILOMETERS @@ -117,7 +118,7 @@ class DemoGeolocationEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the event.""" return self._name @@ -127,17 +128,17 @@ class DemoGeolocationEvent(GeolocationEvent): return False @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 2ea360ebb81..ea315707dea 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -6,6 +6,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_TVSHOW, REPEAT_MODE_OFF, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -40,6 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Bedroom", "kxopViU98Xo", "Epic sax guy 10 hours", 360000 ), DemoMusicPlayer(), + DemoMusicPlayer("Kitchen"), DemoTVShowPlayer(), ] ) @@ -73,6 +75,7 @@ MUSIC_PLAYER_SUPPORT = ( | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_GROUPING | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_REPEAT_SET @@ -291,7 +294,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): class DemoMusicPlayer(AbstractDemoPlayer): - """A Demo media player that only supports YouTube.""" + """A Demo media player.""" # We only implement the methods that we support @@ -318,12 +321,18 @@ class DemoMusicPlayer(AbstractDemoPlayer): ), ] - def __init__(self): + def __init__(self, name="Walkman"): """Initialize the demo device.""" - super().__init__("Walkman") + super().__init__(name) self._cur_track = 0 + self._group_members = [] self._repeat = REPEAT_MODE_OFF + @property + def group_members(self): + """List of players which are currently grouped together.""" + return self._group_members + @property def media_content_id(self): """Return the content ID of current playing media.""" @@ -398,6 +407,18 @@ class DemoMusicPlayer(AbstractDemoPlayer): self._repeat = repeat self.schedule_update_ha_state() + def join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" + self._group_members = [ + self.entity_id, + ] + group_members + self.schedule_update_ha_state() + + def unjoin_player(self): + """Remove this player from any group.""" + self._group_members = [] + self.schedule_update_ha_state() + class DemoTVShowPlayer(AbstractDemoPlayer): """A Demo media player that only supports YouTube.""" diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 9d12621fef1..98e949f38c3 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -49,7 +49,7 @@ class DemoRemote(RemoteEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" if self._last_command_sent is not None: return {"last_command_sent": self._last_command_sent} diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 99aadf356d7..7607bad4e1c 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,12 +1,15 @@ """Demo platform that has a couple of fake sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from . import DOMAIN @@ -31,6 +34,22 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= PERCENTAGE, None, ), + DemoSensor( + "sensor_3", + "Carbon monoxide", + 54, + DEVICE_CLASS_CO, + CONCENTRATION_PARTS_PER_MILLION, + None, + ), + DemoSensor( + "sensor_4", + "Carbon dioxide", + 54, + DEVICE_CLASS_CO2, + CONCENTRATION_PARTS_PER_MILLION, + 14, + ), ] ) @@ -40,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoSensor(Entity): +class DemoSensor(SensorEntity): """Representation of a Demo sensor.""" def __init__( @@ -96,7 +115,7 @@ class DemoSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._battery: return {ATTR_BATTERY_LEVEL: self._battery} diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index e0367fad6a9..0497e2335d3 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -1,5 +1,5 @@ """Support for the demo for speech to text service.""" -from typing import List +from __future__ import annotations from aiohttp import StreamReader @@ -25,32 +25,32 @@ class DemoProvider(Provider): """Demo speech API provider.""" @property - def supported_languages(self) -> List[str]: + def supported_languages(self) -> list[str]: """Return a list of supported languages.""" return SUPPORT_LANGUAGES @property - def supported_formats(self) -> List[AudioFormats]: + def supported_formats(self) -> list[AudioFormats]: """Return a list of supported formats.""" return [AudioFormats.WAV] @property - def supported_codecs(self) -> List[AudioCodecs]: + def supported_codecs(self) -> list[AudioCodecs]: """Return a list of supported codecs.""" return [AudioCodecs.PCM] @property - def supported_bit_rates(self) -> List[AudioBitRates]: + def supported_bit_rates(self) -> list[AudioBitRates]: """Return a list of supported bit rates.""" return [AudioBitRates.BITRATE_16] @property - def supported_sample_rates(self) -> List[AudioSampleRates]: + def supported_sample_rates(self) -> list[AudioSampleRates]: """Return a list of supported sample rates.""" return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100] @property - def supported_channels(self) -> List[AudioChannels]: + def supported_channels(self) -> list[AudioChannels]: """Return a list of supported channels.""" return [AudioChannels.CHANNEL_STEREO] diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json new file mode 100644 index 00000000000..8adbeb3e3c4 --- /dev/null +++ b/homeassistant/components/demo/translations/id.json @@ -0,0 +1,21 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "bool": "Boolean opsional", + "constant": "Konstanta", + "int": "Input numerik" + } + }, + "options_2": { + "data": { + "multi": "Pilihan ganda", + "select": "Pilih salah satu opsi", + "string": "Nilai string" + } + } + } + }, + "title": "Demo" +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index ac10172933f..8e7c97f7c3f 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -10,6 +10,7 @@ "options_1": { "data": { "bool": "Optioneel Boolean", + "constant": "Constant", "int": "Numerieke invoer" } }, diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index ebb6b57ce14..39413a1b9f7 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -140,7 +140,7 @@ class DemoVacuum(VacuumEntity): return max(0, min(100, self._battery_level)) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} @@ -288,7 +288,7 @@ class StateDemoVacuum(StateVacuumEntity): return FAN_SPEEDS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index b909dc7c070..82427f8aa16 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -230,7 +230,7 @@ class DenonDevice(MediaPlayerEntity): @property def source_list(self): """Return the list of available input sources.""" - return sorted(list(self._source_list)) + return sorted(self._source_list) @property def media_title(self): diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 73fe0f2152d..ea484a10877 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,5 +1,6 @@ """Support for Denon AVR receivers using their HTTP interface.""" +from contextlib import suppress import logging from homeassistant.components.media_player import MediaPlayerEntity @@ -306,7 +307,7 @@ class DenonDevice(MediaPlayerEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" if ( self._sound_mode_raw is not None @@ -372,11 +373,9 @@ class DenonDevice(MediaPlayerEntity): volume_denon = float((volume * 100) - 80) if volume_denon > 18: volume_denon = float(18) - try: + with suppress(ValueError): if self._receiver.set_volume(volume_denon): self._volume = volume_denon - except ValueError: - pass def mute_volume(self, mute): """Send mute command.""" diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index f52e6303091..e95aeb10b17 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + "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." }, "step": { "select": { diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index aa56cb47741..41a1910bd56 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -2,10 +2,18 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet" }, "error": { "discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t" + }, + "step": { + "user": { + "data": { + "host": "IP c\u00edm" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/id.json b/homeassistant/components/denonavr/translations/id.json new file mode 100644 index 00000000000..d78f547ef35 --- /dev/null +++ b/homeassistant/components/denonavr/translations/id.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal menyambungkan, coba lagi. Pemutusan sambungan daya listrik dan kabel ethernet lalu menyambungkannya kembali mungkin dapat membantu", + "not_denonavr_manufacturer": "Bukan Network Receiver Denon AVR, pabrikan yang ditemukan tidak sesuai", + "not_denonavr_missing": "Bukan Network Receiver AVR Denon, informasi penemuan tidak lengkap" + }, + "error": { + "discovery_error": "Gagal menemukan Network Receiver AVR Denon" + }, + "flow_title": "Network Receiver Denon AVR: {name}", + "step": { + "confirm": { + "description": "Konfirmasikan penambahan Receiver", + "title": "Network Receiver Denon AVR" + }, + "select": { + "data": { + "select_host": "Alamat IP Receiver" + }, + "description": "Jalankan penyiapan lagi jika ingin menghubungkan Receiver lainnya", + "title": "Pilih Receiver yang ingin dihubungkan" + }, + "user": { + "data": { + "host": "Alamat IP" + }, + "description": "Hubungkan ke Receiver Anda. Jika alamat IP tidak ditentukan, penemuan otomatis akan digunakan", + "title": "Network Receiver Denon AVR" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Tampilkan semua sumber", + "zone2": "Siapkan Zona 2", + "zone3": "Siapkan Zona 3" + }, + "description": "Tentukan pengaturan opsional", + "title": "Network Receiver Denon AVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json index 71562ac53a4..c0121a1e2ca 100644 --- a/homeassistant/components/denonavr/translations/ko.json +++ b/homeassistant/components/denonavr/translations/ko.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc8fc\uc804\uc6d0 \ubc0f \uc774\ub354\ub137 \ucf00\uc774\ube14\uc744 \ubd84\ub9ac\ud55c \ud6c4 \ub2e4\uc2dc \uc5f0\uacb0\ud558\uba74 \ub3c4\uc6c0\uc774 \ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4", "not_denonavr_manufacturer": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uc81c\uc870\uc0ac\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "not_denonavr_missing": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \uac80\uc0c9 \uc815\ubcf4\uac00 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index 6a00e03765f..04f067c2f2a 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -2,11 +2,18 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang" + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Verbinding mislukt, probeer het opnieuw, de stroom- en ethernetkabels loskoppelen en opnieuw aansluiten kan helpen", + "not_denonavr_manufacturer": "Geen Denon AVR Netwerk Receiver, ontdekte fabrikant komt niet overeen", + "not_denonavr_missing": "Geen Denon AVR netwerkontvanger, zoekinformatie niet compleet" + }, + "error": { + "discovery_error": "Kan een Denon AVR netwerkontvanger niet vinden" }, "flow_title": "Denon AVR Network Receiver: {name}", "step": { "confirm": { + "description": "Bevestig het toevoegen van de ontvanger", "title": "Denon AVR Network Receivers" }, "select": { @@ -20,13 +27,20 @@ "data": { "host": "IP-adres" }, - "description": "Maak verbinding met uw ontvanger. Als het IP-adres niet is ingesteld, wordt automatische detectie gebruikt" + "description": "Maak verbinding met uw ontvanger. Als het IP-adres niet is ingesteld, wordt automatische detectie gebruikt", + "title": "Denon AVR Netwerk Ontvangers" } } }, "options": { "step": { "init": { + "data": { + "show_all_sources": "Toon alle bronnen", + "zone2": "Stel Zone 2 in", + "zone3": "Stel Zone 3 in" + }, + "description": "Optionele instellingen opgeven", "title": "Denon AVR Network Receivers" } } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 7c2cc444a5a..12fc4ddd7ba 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -86,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([derivative]) -class DerivativeSensor(RestoreEntity): +class DerivativeSensor(RestoreEntity, SensorEntity): """Representation of an derivative sensor.""" def __init__( @@ -211,7 +211,7 @@ class DerivativeSensor(RestoreEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_SOURCE_ID: self._sensor_source_id} diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index b3e4cd432ac..33fd9a8224f 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -4,10 +4,9 @@ from datetime import timedelta import schiene import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_OFFSET import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util CONF_DESTINATION = "to" @@ -40,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([DeutscheBahnSensor(start, destination, offset, only_direct)], True) -class DeutscheBahnSensor(Entity): +class DeutscheBahnSensor(SensorEntity): """Implementation of a Deutsche Bahn sensor.""" def __init__(self, start, goal, offset, only_direct): @@ -65,7 +64,7 @@ class DeutscheBahnSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" connections = self.data.connections[0] if len(self.data.connections) > 1: diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index bba02147c71..4741dbdb7f5 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,8 +1,10 @@ """Helpers for device automations.""" +from __future__ import annotations + import asyncio from functools import wraps from types import ModuleType -from typing import Any, List, MutableMapping +from typing import Any, MutableMapping import voluptuous as vol import voluptuous_serialize @@ -116,7 +118,7 @@ async def _async_get_device_automations(hass, automation_type, device_id): ) domains = set() - automations: List[MutableMapping[str, Any]] = [] + automations: list[MutableMapping[str, Any]] = [] device = device_registry.async_get(device_id) if device is None: diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 61c50da6868..72fcc9790b2 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,5 +1,7 @@ """Device automation helpers for toggle entity.""" -from typing import Any, Dict, List +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -149,15 +151,12 @@ async def async_attach_trigger( """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type == CONF_TURNED_ON: - from_state = "off" to_state = "on" else: - from_state = "on" to_state = "off" state_config = { CONF_PLATFORM: "state", state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state_trigger.CONF_FROM: from_state, state_trigger.CONF_TO: to_state, } if CONF_FOR in config: @@ -170,10 +169,10 @@ async def async_attach_trigger( async def _async_get_automations( - hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str -) -> List[dict]: + hass: HomeAssistant, device_id: str, automation_templates: list[dict], domain: str +) -> list[dict]: """List device automations.""" - automations: List[Dict[str, Any]] = [] + automations: list[dict[str, Any]] = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() entries = [ @@ -198,21 +197,21 @@ async def _async_get_automations( async def async_get_actions( hass: HomeAssistant, device_id: str, domain: str -) -> List[dict]: +) -> list[dict]: """List device actions.""" return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) async def async_get_conditions( hass: HomeAssistant, device_id: str, domain: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) async def async_get_triggers( hass: HomeAssistant, device_id: str, domain: str -) -> List[dict]: +) -> list[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index d785ee826e8..dfdfd678c0f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,16 +1,11 @@ """Provide functionality to keep track of devices.""" -from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import - ATTR_GPS_ACCURACY, - STATE_HOME, -) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401 +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .config_entry import ( # noqa: F401 pylint: disable=unused-import - async_setup_entry, - async_unload_entry, -) -from .const import ( # noqa: F401 pylint: disable=unused-import +from .config_entry import async_setup_entry, async_unload_entry # noqa: F401 +from .const import ( # noqa: F401 ATTR_ATTRIBUTES, ATTR_BATTERY, ATTR_DEV_ID, @@ -29,7 +24,7 @@ from .const import ( # noqa: F401 pylint: disable=unused-import SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, ) -from .legacy import ( # noqa: F401 pylint: disable=unused-import +from .legacy import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, SERVICE_SEE, @@ -42,12 +37,12 @@ from .legacy import ( # noqa: F401 pylint: disable=unused-import @bind_hass -def is_on(hass: HomeAssistantType, entity_id: str): +def is_on(hass: HomeAssistant, entity_id: str): """Return the state if any or a specified device is home.""" return hass.states.is_state(entity_id, STATE_HOME) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the device tracker.""" await async_setup_legacy_integration(hass, config) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 16e7d022c92..05fa4b4a60d 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -1,5 +1,7 @@ """Code to set up a device tracker platform using a config entry.""" -from typing import Optional +from __future__ import annotations + +from typing import final from homeassistant.components import zone from homeassistant.const import ( @@ -18,7 +20,7 @@ from .const import ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, async def async_setup_entry(hass, entry): """Set up an entry.""" - component: Optional[EntityComponent] = hass.data.get(DOMAIN) + component: EntityComponent | None = hass.data.get(DOMAIN) if component is None: component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass) @@ -59,7 +61,7 @@ class BaseTrackerEntity(Entity): class TrackerEntity(BaseTrackerEntity): - """Represent a tracked device.""" + """Base class for a tracked device.""" @property def should_poll(self): @@ -114,6 +116,7 @@ class TrackerEntity(BaseTrackerEntity): return None + @final @property def state_attributes(self): """Return the device state attributes.""" @@ -128,7 +131,7 @@ class TrackerEntity(BaseTrackerEntity): class ScannerEntity(BaseTrackerEntity): - """Represent a tracked device that is on a scanned network.""" + """Base class for a tracked device that is on a scanned network.""" @property def ip_address(self) -> str: @@ -157,6 +160,7 @@ class ScannerEntity(BaseTrackerEntity): """Return true if the device is connected to the network.""" raise NotImplementedError + @final @property def state_attributes(self): """Return the device state attributes.""" diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 9c102bfa745..0260a4bbd3a 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -1,5 +1,5 @@ """Provides device automations for Device tracker.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -32,7 +32,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Device tracker devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 2c92304a246..81a16545c74 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Device Tracker.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -32,7 +32,7 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Device Tracker devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -71,8 +71,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] == "enters": event = zone.EVENT_ENTER else: diff --git a/homeassistant/components/device_tracker/group.py b/homeassistant/components/device_tracker/group.py index 07ec2cfe985..9bd2c991678 100644 --- a/homeassistant/components/device_tracker/group.py +++ b/homeassistant/components/device_tracker/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/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index b7583d80f82..eae133965c6 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -1,9 +1,11 @@ """Legacy device tracker classes.""" +from __future__ import annotations + import asyncio from datetime import timedelta import hashlib from types import ModuleType -from typing import Any, Callable, Dict, List, Optional, Sequence +from typing import Any, Callable, Sequence, final import attr import voluptuous as vol @@ -25,7 +27,7 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv @@ -35,7 +37,7 @@ from homeassistant.helpers.event import ( async_track_utc_time_change, ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType, GPSType from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import dt as dt_util from homeassistant.util.yaml import dump @@ -117,7 +119,7 @@ EVENT_NEW_DEVICE = "device_tracker_new_device" def see( - hass: HomeAssistantType, + hass: HomeAssistant, mac: str = None, dev_id: str = None, host_name: str = None, @@ -146,7 +148,7 @@ def see( hass.services.call(DOMAIN, SERVICE_SEE, data) -async def async_setup_integration(hass: HomeAssistantType, config: ConfigType) -> None: +async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None: """Set up the legacy integration.""" tracker = await get_tracker(hass, config) @@ -205,7 +207,7 @@ class DeviceTrackerPlatform: name: str = attr.ib() platform: ModuleType = attr.ib() - config: Dict = attr.ib() + config: dict = attr.ib() @property def type(self): @@ -219,7 +221,7 @@ 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.type) + LOGGER.info("Setting up %s.%s", DOMAIN, self.name) try: scanner = None setup = None @@ -246,6 +248,9 @@ class DeviceTrackerPlatform: 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 @@ -253,11 +258,11 @@ class DeviceTrackerPlatform: return if not setup: - LOGGER.error("Error setting up platform %s", self.type) + 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", self.type) + LOGGER.exception("Error setting up platform %s %s", self.type, self.name) async def async_extract_config(hass, config): @@ -285,7 +290,7 @@ async def async_extract_config(hass, config): async def async_create_platform_type( hass, config, p_type, p_config -) -> Optional[DeviceTrackerPlatform]: +) -> DeviceTrackerPlatform | None: """Determine type of platform.""" platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) @@ -297,7 +302,7 @@ async def async_create_platform_type( @callback def async_setup_scanner_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, scanner: Any, async_see_device: Callable, @@ -387,7 +392,7 @@ class DeviceTracker: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, consider_home: timedelta, track_new: bool, defaults: dict, @@ -586,7 +591,7 @@ class DeviceTracker: class Device(RestoreEntity): - """Represent a tracked device.""" + """Base class for a tracked device.""" host_name: str = None location_name: str = None @@ -604,7 +609,7 @@ class Device(RestoreEntity): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, consider_home: timedelta, track: bool, dev_id: str, @@ -659,6 +664,7 @@ class Device(RestoreEntity): """Return the picture of the device.""" return self.config_picture + @final @property def state_attributes(self): """Return the device state attributes.""" @@ -675,7 +681,7 @@ class Device(RestoreEntity): return attributes @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" return self._attributes @@ -784,9 +790,9 @@ class Device(RestoreEntity): class DeviceScanner: """Device scanner object.""" - hass: HomeAssistantType = None + hass: HomeAssistant = None - def scan_devices(self) -> List[str]: + def scan_devices(self) -> list[str]: """Scan for devices.""" raise NotImplementedError() @@ -811,9 +817,7 @@ class DeviceScanner: return await self.hass.async_add_executor_job(self.get_extra_attributes, device) -async def async_load_config( - path: str, hass: HomeAssistantType, consider_home: timedelta -): +async def async_load_config(path: str, hass: HomeAssistant, consider_home: timedelta): """Load devices from YAML configuration file. This method is a coroutine. diff --git a/homeassistant/components/device_tracker/translations/id.json b/homeassistant/components/device_tracker/translations/id.json index 99baa5e1a76..be5c7e932ce 100644 --- a/homeassistant/components/device_tracker/translations/id.json +++ b/homeassistant/components/device_tracker/translations/id.json @@ -1,7 +1,17 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} ada di rumah", + "is_not_home": "{entity_name} tidak ada di rumah" + }, + "trigger_type": { + "enters": "{entity_name} memasuki zona", + "leaves": "{entity_name} meninggalkan zona" + } + }, "state": { "_": { - "home": "Rumah", + "home": "Di Rumah", "not_home": "Keluar" } }, diff --git a/homeassistant/components/device_tracker/translations/it.json b/homeassistant/components/device_tracker/translations/it.json index 92ccce1c1c5..646f0732cd8 100644 --- a/homeassistant/components/device_tracker/translations/it.json +++ b/homeassistant/components/device_tracker/translations/it.json @@ -15,5 +15,5 @@ "not_home": "Fuori casa" } }, - "title": "Tracciatore dispositivo" + "title": "Localizzatore di dispositivi" } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/ko.json b/homeassistant/components/device_tracker/translations/ko.json index e3e72d49c89..538db691b4f 100644 --- a/homeassistant/components/device_tracker/translations/ko.json +++ b/homeassistant/components/device_tracker/translations/ko.json @@ -1,8 +1,12 @@ { "device_automation": { "condition_type": { - "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc73c\uba74", - "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74" + "is_home": "{entity_name}\uc774(\uac00) \uc9d1\uc5d0 \uc788\uc73c\uba74", + "is_not_home": "{entity_name}\uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74" + }, + "trigger_type": { + "enters": "{entity_name}\uc774(\uac00) \uc9c0\uc5ed\uc5d0 \ub4e4\uc5b4\uac08 \ub54c", + "leaves": "{entity_name}\uc774(\uac00) \uc9c0\uc5ed\uc5d0\uc11c \ub098\uc62c \ub54c" } }, "state": { diff --git a/homeassistant/components/device_tracker/translations/nl.json b/homeassistant/components/device_tracker/translations/nl.json index a28c8bdbbb8..a3f7d6cd31f 100644 --- a/homeassistant/components/device_tracker/translations/nl.json +++ b/homeassistant/components/device_tracker/translations/nl.json @@ -15,5 +15,5 @@ "not_home": "Afwezig" } }, - "title": "Apparaat tracker" + "title": "Apparaattracker" } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/zh-Hans.json b/homeassistant/components/device_tracker/translations/zh-Hans.json index c019a3dcda8..5d56de9b855 100644 --- a/homeassistant/components/device_tracker/translations/zh-Hans.json +++ b/homeassistant/components/device_tracker/translations/zh-Hans.json @@ -5,8 +5,8 @@ "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" }, "trigger_type": { - "enters": "{entity_name} \u8fdb\u5165\u533a\u57df", - "leaves": "{entity_name} \u79bb\u5f00\u533a\u57df" + "enters": "{entity_name} \u8fdb\u5165\u6307\u5b9a\u533a\u57df", + "leaves": "{entity_name} \u79bb\u5f00\u6307\u5b9a\u533a\u57df" } }, "state": { diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index e5ee9029302..2fb31c6291c 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -15,11 +15,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS -async def async_setup(hass, config): - """Get all devices and add them to hass.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the devolo account from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -54,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool ) ) ) - except (ConnectionError, GatewayOfflineError) as err: + except GatewayOfflineError as err: raise ConfigEntryNotReady from err for platform in PLATFORMS: diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index d333a5c7609..7ad375bf44d 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -1,5 +1,5 @@ """Platform for climate integration.""" -from typing import List, Optional +from __future__ import annotations from homeassistant.components.climate import ( ATTR_TEMPERATURE, @@ -45,7 +45,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit """Representation of a climate/thermostat device within devolo Home Control.""" @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" if hasattr(self._device_instance, "multi_level_sensor_property"): return next( @@ -60,7 +60,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit return None @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the target temperature.""" return self._value @@ -75,7 +75,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit return HVAC_MODE_HEAT @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return [HVAC_MODE_HEAT] diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 3f51a9c0884..d6dbd331d5f 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import ( # pylint:disable=unused-import - CONF_MYDEVOLO, - DEFAULT_MYDEVOLO, - DOMAIN, -) +from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index d0be9543bf4..6aef842ffff 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -18,6 +18,7 @@ class DevoloDeviceEntity(Entity): self._unique_id = element_uid self._homecontrol = homecontrol self._name = device_instance.settings_property["general_device_settings"].name + self._area = device_instance.settings_property["general_device_settings"].zone self._device_class = None self._value = None self._unit = None @@ -59,6 +60,7 @@ class DevoloDeviceEntity(Entity): "name": self._name, "manufacturer": self._brand, "model": self._model, + "suggested_area": self._area, } @property diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 0ffa991493c..93cf4be5d35 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.16.0"], + "requirements": ["devolo-home-control-api==0.17.1"], "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index e78b4eabeac..e3c16670dfd 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -5,6 +5,8 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + SensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -20,6 +22,7 @@ DEVICE_CLASS_MAPPING = { "humidity": DEVICE_CLASS_HUMIDITY, "current": DEVICE_CLASS_POWER, "total": DEVICE_CLASS_POWER, + "voltage": DEVICE_CLASS_VOLTAGE, } @@ -63,7 +66,7 @@ async def async_setup_entry( async_add_entities(entities, False) -class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): +class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity): """Abstract representation of a multi level sensor within devolo Home Control.""" @property diff --git a/homeassistant/components/devolo_home_control/translations/he.json b/homeassistant/components/devolo_home_control/translations/he.json index 3007c0e968c..ac90b3264ea 100644 --- a/homeassistant/components/devolo_home_control/translations/he.json +++ b/homeassistant/components/devolo_home_control/translations/he.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json index ff2c2fc87b5..45b07f0adcb 100644 --- a/homeassistant/components/devolo_home_control/translations/hu.json +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { "user": { "data": { - "password": "Jelsz\u00f3" + "home_control_url": "Home Control URL", + "mydevolo_url": "mydevolo URL", + "password": "Jelsz\u00f3", + "username": "E-mail / devolo ID" } } } diff --git a/homeassistant/components/devolo_home_control/translations/id.json b/homeassistant/components/devolo_home_control/translations/id.json new file mode 100644 index 00000000000..8b7ce0171d5 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "home_control_url": "URL Home Control", + "mydevolo_url": "URL mydevolo", + "password": "Kata Sandi", + "username": "Email/ID devolo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index c2eb9bd466d..1630d4b9dfd 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -26,12 +26,6 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=180) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up configured Dexcom.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Dexcom from a config entry.""" try: @@ -57,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except SessionError as error: raise UpdateFailed(error) from error + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: DataUpdateCoordinator( hass, @@ -68,11 +63,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: entry.add_update_listener(update_listener), } - await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_refresh() + await hass.data[DOMAIN][entry.entry_id][ + COORDINATOR + ].async_config_entry_first_refresh() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -83,8 +80,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py index 1d6d52fa0c9..0f3aad6f813 100644 --- a/homeassistant/components/dexcom/config_flow.py +++ b/homeassistant/components/dexcom/config_flow.py @@ -6,14 +6,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import callback -from .const import ( # pylint:disable=unused-import - CONF_SERVER, - DOMAIN, - MG_DL, - MMOL_L, - SERVER_OUS, - SERVER_US, -) +from .const import CONF_SERVER, DOMAIN, MG_DL, MMOL_L, SERVER_OUS, SERVER_US DATA_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index afb3feeec4d..730a1824e1a 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -1,4 +1,5 @@ """Support for Dexcom sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -16,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class DexcomGlucoseValueSensor(CoordinatorEntity): +class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity): """Representation of a Dexcom glucose value sensor.""" def __init__(self, coordinator, username, unit_of_measurement): @@ -58,7 +59,7 @@ class DexcomGlucoseValueSensor(CoordinatorEntity): return self._unique_id -class DexcomGlucoseTrendSensor(CoordinatorEntity): +class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity): """Representation of a Dexcom glucose trend sensor.""" def __init__(self, coordinator, username): diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index d567dd6b611..64cdc05a081 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Passwort", + "server": "Server", "username": "Benutzername" } } diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json index 7a67a978ae1..45f38b22a84 100644 --- a/homeassistant/components/dexcom/translations/hu.json +++ b/homeassistant/components/dexcom/translations/hu.json @@ -4,7 +4,27 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "server": "Szerver", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/id.json b/homeassistant/components/dexcom/translations/id.json new file mode 100644 index 00000000000..2802216e782 --- /dev/null +++ b/homeassistant/components/dexcom/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "server": "Server", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial Dexcom Share", + "title": "Siapkan integrasi Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Satuan pengukuran" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/nl.json b/homeassistant/components/dexcom/translations/nl.json index 1dd597d28b4..9be09aff0c8 100644 --- a/homeassistant/components/dexcom/translations/nl.json +++ b/homeassistant/components/dexcom/translations/nl.json @@ -12,7 +12,19 @@ "user": { "data": { "password": "Wachtwoord", + "server": "Server", "username": "Gebruikersnaam" + }, + "description": "Voer Dexcom Share-gegevens in", + "title": "Dexcom integratie instellen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Meeteenheid" } } } diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json index aa90d6d998d..08543cf103e 100644 --- a/homeassistant/components/dexcom/translations/ru.json +++ b/homeassistant/components/dexcom/translations/ru.json @@ -13,7 +13,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "server": "\u0421\u0435\u0440\u0432\u0435\u0440", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Dexcom" diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index d33c6159888..e21b6cf88dc 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -1,16 +1,24 @@ """The dhcp integration.""" from abc import abstractmethod +from datetime import timedelta import fnmatch from ipaddress import ip_address as make_ip_address import logging import os import threading +from aiodiscover import DiscoverHosts +from aiodiscover.discovery import ( + HOSTNAME as DISCOVERY_HOSTNAME, + IP_ADDRESS as DISCOVERY_IP_ADDRESS, + MAC_ADDRESS as DISCOVERY_MAC_ADDRESS, +) from scapy.arch.common import compile_filter from scapy.config import conf from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP +from scapy.layers.inet import IP from scapy.layers.l2 import Ether from scapy.sendrecv import AsyncSniffer @@ -29,9 +37,12 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.event import async_track_state_added_domain +from homeassistant.helpers.event import ( + async_track_state_added_domain, + async_track_time_interval, +) from homeassistant.loader import async_get_dhcp -from homeassistant.util.network import is_link_local +from homeassistant.util.network import is_invalid, is_link_local, is_loopback from .const import DOMAIN @@ -42,6 +53,7 @@ HOSTNAME = "hostname" MAC_ADDRESS = "macaddress" IP_ADDRESS = "ip" DHCP_REQUEST = 3 +SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__name__) @@ -54,7 +66,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: integration_matchers = await async_get_dhcp(hass) watchers = [] - for cls in (DHCPWatcher, DeviceTrackerWatcher): + for cls in (DHCPWatcher, DeviceTrackerWatcher, NetworkWatcher): watcher = cls(hass, address_data, integration_matchers) await watcher.async_start() watchers.append(watcher) @@ -82,13 +94,23 @@ class WatcherBase: def process_client(self, ip_address, hostname, mac_address): """Process a client.""" - if is_link_local(make_ip_address(ip_address)): - # Ignore self assigned addresses + made_ip_address = make_ip_address(ip_address) + + if ( + is_link_local(made_ip_address) + or is_loopback(made_ip_address) + or is_invalid(made_ip_address) + ): + # Ignore self assigned addresses, loopback, invalid return data = self._address_data.get(ip_address) - if data and data[MAC_ADDRESS] == mac_address and data[HOSTNAME] == hostname: + if ( + data + and data[MAC_ADDRESS] == mac_address + and data[HOSTNAME].startswith(hostname) + ): # If the address data is the same no need # to process it return @@ -126,7 +148,11 @@ class WatcherBase: self.hass.config_entries.flow.async_init( entry["domain"], context={"source": DOMAIN}, - data={IP_ADDRESS: ip_address, **data}, + data={ + IP_ADDRESS: ip_address, + HOSTNAME: lowercase_hostname, + MAC_ADDRESS: data[MAC_ADDRESS], + }, ) ) @@ -135,6 +161,54 @@ class WatcherBase: """Pass a task to async_add_task based on which context we are in.""" +class NetworkWatcher(WatcherBase): + """Class to query ptr records routers.""" + + def __init__(self, hass, address_data, integration_matchers): + """Initialize class.""" + super().__init__(hass, address_data, integration_matchers) + self._unsub = None + self._discover_hosts = None + self._discover_task = None + + async def async_stop(self): + """Stop scanning for new devices on the network.""" + if self._unsub: + self._unsub() + self._unsub = None + if self._discover_task: + self._discover_task.cancel() + self._discover_task = None + + async def async_start(self): + """Start scanning for new devices on the network.""" + self._discover_hosts = DiscoverHosts() + self._unsub = async_track_time_interval( + self.hass, self.async_start_discover, SCAN_INTERVAL + ) + self.async_start_discover() + + @callback + def async_start_discover(self, *_): + """Start a new discovery task if one is not running.""" + if self._discover_task and not self._discover_task.done(): + return + self._discover_task = self.create_task(self.async_discover()) + + async def async_discover(self): + """Process discovery.""" + for host in await self._discover_hosts.async_discover(): + self.process_client( + host[DISCOVERY_IP_ADDRESS], + host[DISCOVERY_HOSTNAME], + _format_mac(host[DISCOVERY_MAC_ADDRESS]), + ) + + def create_task(self, task): + """Pass a task to async_create_task since we are in async context.""" + return self.hass.async_create_task(task) + + class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" @@ -184,7 +258,7 @@ class DeviceTrackerWatcher(WatcherBase): def create_task(self, task): """Pass a task to async_create_task since we are in async context.""" - self.hass.async_create_task(task) + return self.hass.async_create_task(task) class DHCPWatcher(WatcherBase): @@ -207,8 +281,11 @@ class DHCPWatcher(WatcherBase): async def async_start(self): """Start watching for dhcp packets.""" + # disable scapy promiscuous mode as we do not need it + conf.sniff_promisc = 0 + try: - _verify_l2socket_creation_permission() + await self.hass.async_add_executor_job(_verify_l2socket_setup, FILTER) except (Scapy_Exception, OSError) as ex: if os.geteuid() == 0: _LOGGER.error("Cannot watch for dhcp packets: %s", ex) @@ -219,7 +296,7 @@ class DHCPWatcher(WatcherBase): return try: - await _async_verify_working_pcap(self.hass, FILTER) + await self.hass.async_add_executor_job(_verify_working_pcap, FILTER) except (Scapy_Exception, ImportError) as ex: _LOGGER.error( "Cannot watch for dhcp packets without a functional packet filter: %s", @@ -233,6 +310,7 @@ class DHCPWatcher(WatcherBase): prn=self.handle_dhcp_packet, store=0, ) + self._sniffer.start() def handle_dhcp_packet(self, packet): @@ -247,7 +325,7 @@ class DHCPWatcher(WatcherBase): # DHCP request return - ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) + ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src hostname = _decode_dhcp_option(options, HOSTNAME) mac_address = _format_mac(packet[Ether].src) @@ -258,7 +336,7 @@ class DHCPWatcher(WatcherBase): def create_task(self, task): """Pass a task to hass.add_job since we are in a thread.""" - self.hass.add_job(task) + return self.hass.add_job(task) def _decode_dhcp_option(dhcp_options, key): @@ -283,7 +361,7 @@ def _format_mac(mac_address): return format_mac(mac_address).replace(":", "") -def _verify_l2socket_creation_permission(): +def _verify_l2socket_setup(cap_filter): """Create a socket using the scapy configured l2socket. Try to create the socket @@ -292,15 +370,13 @@ def _verify_l2socket_creation_permission(): thread so we will not be able to capture any permission or bind errors. """ - # disable scapy promiscuous mode as we do not need it - conf.sniff_promisc = 0 - conf.L2socket() + conf.L2socket(filter=cap_filter) -async def _async_verify_working_pcap(hass, cap_filter): +def _verify_working_pcap(cap_filter): """Verify we can create a packet filter. If we cannot create a filter we will be listening for all traffic which is too intensive. """ - await hass.async_add_executor_job(compile_filter, cap_filter) + compile_filter(cap_filter) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index eda229ebec7..80cc6b116c9 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,7 @@ "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", "requirements": [ - "scapy==2.4.4" + "scapy==2.4.4", "aiodiscover==1.3.3" ], "codeowners": [ "@bdraco" diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 0bddd5a187e..602a0f2b76f 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -1,11 +1,12 @@ """Support for Adafruit DHT temperature and humidity sensor.""" +from contextlib import suppress from datetime import timedelta import logging import Adafruit_DHT # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, @@ -14,7 +15,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dev = [] name = config[CONF_NAME] - try: + with suppress(KeyError): for variable in config[CONF_MONITORED_CONDITIONS]: dev.append( DHTSensor( @@ -87,13 +87,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): humidity_offset, ) ) - except KeyError: - pass add_entities(dev, True) -class DHTSensor(Entity): +class DHTSensor(SensorEntity): """Implementation of the DHT sensor.""" def __init__( diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index e623919f099..6003f17c9e9 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -24,11 +24,6 @@ class DialogFlowError(HomeAssistantError): """Raised when a DialogFlow error happens.""" -async def async_setup(hass, config): - """Set up the Dialogflow component.""" - return True - - async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook with Dialogflow requests.""" message = await request.json() diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json index 04427a1efed..17f38b0262f 100644 --- a/homeassistant/components/dialogflow/translations/hu.json +++ b/homeassistant/components/dialogflow/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t] ( {dialogflow_url} ). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/translations/id.json b/homeassistant/components/dialogflow/translations/id.json new file mode 100644 index 00000000000..046a04b1dc4 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [integrasi webhook dengan Dialogflow]({dialogflow_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan Dialogflow?", + "title": "Siapkan Dialogflow Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/ko.json b/homeassistant/components/dialogflow/translations/ko.json index 2b1be9657b4..e0414018787 100644 --- a/homeassistant/components/dialogflow/translations/ko.json +++ b/homeassistant/components/dialogflow/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow \uc6f9 \ud6c5]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow \uc6f9 \ud6c5]({dialogflow_url})\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index eb1345df45c..9a9f82c36d2 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -79,7 +79,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity): return DEVICE_CLASS_MOVING @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index 811b844e35c..0678b9ab1a1 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -71,7 +71,7 @@ class DigitalOceanSwitch(SwitchEntity): return self.data.status == "active" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 22a97b9e82e..f1f05e815a8 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -1,7 +1,9 @@ """The DirecTV integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta -from typing import Any, Dict +from typing import Any from directv import DIRECTV, DIRECTVError @@ -28,12 +30,6 @@ PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) -async def async_setup(hass: HomeAssistant, config: Dict) -> bool: - """Set up the DirecTV component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DirecTV from a config entry.""" dtv = DIRECTV(entry.data[CONF_HOST], session=async_get_clientsession(hass)) @@ -43,11 +39,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except DIRECTVError as err: raise ConfigEntryNotReady from err + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = dtv - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -58,8 +55,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -87,7 +84,7 @@ class DIRECTVEntity(Entity): return self._name @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this DirecTV receiver.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index fed13c63dc8..71a8e052c47 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -1,6 +1,8 @@ """Config flow for DirecTV.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from urllib.parse import urlparse from directv import DIRECTV, DIRECTVError @@ -16,8 +18,7 @@ from homeassistant.helpers.typing import ( HomeAssistantType, ) -from .const import CONF_RECEIVER_ID -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_RECEIVER_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,7 +26,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: HomeAssistantType, 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,8 +49,8 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = {} async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -71,7 +72,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp( self, discovery_info: DiscoveryInfoType - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle SSDP discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname receiver_id = None @@ -104,7 +105,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: ConfigType = None - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: return self.async_show_form( @@ -118,7 +119,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index dfd88ca885b..4004592e5dc 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,6 +1,8 @@ """Support for the DirecTV receivers.""" +from __future__ import annotations + import logging -from typing import Callable, List, Optional +from typing import Callable from directv import DIRECTV @@ -64,7 +66,7 @@ SUPPORT_DTV_CLIENT = ( async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List, bool], None], + async_add_entities: Callable[[list, bool], None], ) -> bool: """Set up the DirecTV config entry.""" dtv = hass.data[DOMAIN][entry.entry_id] @@ -124,7 +126,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._assumed_state = self._is_recorded @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" if self._is_standby: return {} @@ -141,7 +143,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return self._name @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device.""" return DEVICE_CLASS_RECEIVER diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index 64695ae3813..b35580928ac 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -1,7 +1,9 @@ """Support for the DIRECTV remote.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Callable, Iterable, List +from typing import Any, Callable, Iterable from directv import DIRECTV, DIRECTVError @@ -20,7 +22,7 @@ SCAN_INTERVAL = timedelta(minutes=2) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List, bool], None], + async_add_entities: Callable[[list, bool], None], ) -> bool: """Load DirecTV remote based on a config entry.""" dtv = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/directv/translations/hu.json b/homeassistant/components/directv/translations/hu.json index 5d8fc929b92..0309eb35881 100644 --- a/homeassistant/components/directv/translations/hu.json +++ b/homeassistant/components/directv/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/directv/translations/id.json b/homeassistant/components/directv/translations/id.json new file mode 100644 index 00000000000..74f778d6cee --- /dev/null +++ b/homeassistant/components/directv/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/ko.json b/homeassistant/components/directv/translations/ko.json index f2526418d08..ecbde981160 100644 --- a/homeassistant/components/directv/translations/ko.json +++ b/homeassistant/components/directv/translations/ko.json @@ -10,7 +10,7 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { diff --git a/homeassistant/components/directv/translations/nl.json b/homeassistant/components/directv/translations/nl.json index 2024368daf6..95709571234 100644 --- a/homeassistant/components/directv/translations/nl.json +++ b/homeassistant/components/directv/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "DirecTV-ontvanger is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "DirecTV: {name}", "step": { @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Host- of IP-adres" + "host": "Host" } } } diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 4d78b540a0c..81beec0e60e 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -6,7 +6,7 @@ import random import discogs_client import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, @@ -15,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -89,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class DiscogsSensor(Entity): +class DiscogsSensor(SensorEntity): """Create a new Discogs sensor for a specific type.""" def __init__(self, discogs_data, name, sensor_type): @@ -121,7 +120,7 @@ class DiscogsSensor(Entity): return SENSORS[self._type]["unit_of_measurement"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes of the sensor.""" if self._state is None or self._attrs is None: return None diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index b97202033bd..883958226d8 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -1,11 +1,4 @@ -""" -Starts a service to scan in intervals for new devices. - -Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. - -Knows which components handle certain types, will make sure they are -loaded before the EVENT_PLATFORM_DISCOVERED is fired. -""" +"""Starts a service to scan in intervals for new devices.""" from datetime import timedelta import json import logging @@ -29,7 +22,6 @@ SERVICE_APPLE_TV = "apple_tv" SERVICE_DAIKIN = "daikin" SERVICE_DLNA_DMR = "dlna_dmr" SERVICE_ENIGMA2 = "enigma2" -SERVICE_FREEBOX = "freebox" SERVICE_HASS_IOS_APP = "hass_ios" SERVICE_HASSIO = "hassio" SERVICE_HEOS = "heos" @@ -45,25 +37,17 @@ SERVICE_WEMO = "belkin_wemo" SERVICE_WINK = "wink" SERVICE_XIAOMI_GW = "xiaomi_gw" +# These have custom protocols CONFIG_ENTRY_HANDLERS = { - SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", "logitech_mediaserver": "squeezebox", } +# These have no config flows SERVICE_HANDLERS = { - SERVICE_MOBILE_APP: ("mobile_app", None), - SERVICE_HASS_IOS_APP: ("ios", None), SERVICE_NETGEAR: ("device_tracker", None), - SERVICE_HASSIO: ("hassio", None), - SERVICE_APPLE_TV: ("apple_tv", None), SERVICE_ENIGMA2: ("media_player", "enigma2"), - SERVICE_WINK: ("wink", None), SERVICE_SABNZBD: ("sabnzbd", None), - SERVICE_SAMSUNG_PRINTER: ("sensor", None), - SERVICE_KONNECTED: ("konnected", None), - SERVICE_OCTOPRINT: ("octoprint", None), - SERVICE_FREEBOX: ("freebox", None), "yamaha": ("media_player", "yamaha"), "frontier_silicon": ("media_player", "frontier_silicon"), "openhome": ("media_player", "openhome"), @@ -76,20 +60,29 @@ SERVICE_HANDLERS = { OPTIONAL_SERVICE_HANDLERS = {SERVICE_DLNA_DMR: ("media_player", "dlna_dmr")} MIGRATED_SERVICE_HANDLERS = [ + SERVICE_APPLE_TV, "axis", "deconz", + SERVICE_DAIKIN, "denonavr", "esphome", "google_cast", + SERVICE_HASS_IOS_APP, + SERVICE_HASSIO, SERVICE_HEOS, "harmony", "homekit", "ikea_tradfri", "kodi", + SERVICE_KONNECTED, + SERVICE_MOBILE_APP, + SERVICE_OCTOPRINT, "philips_hue", + SERVICE_SAMSUNG_PRINTER, "sonos", "songpal", SERVICE_WEMO, + SERVICE_WINK, SERVICE_XIAOMI_GW, "volumio", SERVICE_YEELIGHT, diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index c173c879ad1..432bc7ec0b3 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -71,7 +71,7 @@ class SmartPlugSwitch(SwitchEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" try: ui_temp = self.units.temperature(int(self.data.temperature), TEMP_CELSIUS) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ac7a4b22e58..094a9adc43a 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -2,6 +2,6 @@ "domain": "dlna_dmr", "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.14.13"], + "requirements": ["async-upnp-client==0.16.0"], "codeowners": [] } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index f8af118caed..c208e1eb2ff 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -1,9 +1,10 @@ """Support for DLNA DMR (Device Media Renderer).""" +from __future__ import annotations + import asyncio from datetime import timedelta import functools import logging -from typing import Optional import aiohttp from async_upnp_client import UpnpFactory @@ -13,14 +14,6 @@ import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_IMAGE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -69,28 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -HOME_ASSISTANT_UPNP_CLASS_MAPPING = { - MEDIA_TYPE_MUSIC: "object.item.audioItem", - MEDIA_TYPE_TVSHOW: "object.item.videoItem", - MEDIA_TYPE_MOVIE: "object.item.videoItem", - MEDIA_TYPE_VIDEO: "object.item.videoItem", - MEDIA_TYPE_EPISODE: "object.item.videoItem", - MEDIA_TYPE_CHANNEL: "object.item.videoItem", - MEDIA_TYPE_IMAGE: "object.item.imageItem", - MEDIA_TYPE_PLAYLIST: "object.item.playlistItem", -} -UPNP_CLASS_DEFAULT = "object.item" -HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = { - MEDIA_TYPE_MUSIC: "audio/*", - MEDIA_TYPE_TVSHOW: "video/*", - MEDIA_TYPE_MOVIE: "video/*", - MEDIA_TYPE_VIDEO: "video/*", - MEDIA_TYPE_EPISODE: "video/*", - MEDIA_TYPE_CHANNEL: "video/*", - MEDIA_TYPE_IMAGE: "image/*", - MEDIA_TYPE_PLAYLIST: "playlist/*", -} - def catch_request_errors(): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" @@ -116,7 +87,7 @@ async def async_start_event_handler( server_host: str, server_port: int, requester, - callback_url_override: Optional[str] = None, + callback_url_override: str | None = None, ): """Register notify view.""" hass_data = hass.data[DLNA_DMR_DATA] @@ -341,20 +312,15 @@ class DlnaDmrDevice(MediaPlayerEntity): @catch_request_errors() async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" + _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs) title = "Home Assistant" - mime_type = HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING.get(media_type, media_type) - upnp_class = HOME_ASSISTANT_UPNP_CLASS_MAPPING.get( - media_type, UPNP_CLASS_DEFAULT - ) # Stop current playing media if self._device.can_stop: await self.async_media_stop() # Queue media - await self._device.async_set_transport_uri( - media_id, title, mime_type, upnp_class - ) + await self._device.async_set_transport_uri(media_id, title) await self._device.async_wait_for_can_play() # If already playing, no need to call Play diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index b202ff8485c..01d6e2f4f2a 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -6,10 +6,9 @@ import aiodns from aiodns.error import DNSError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -55,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N async_add_devices([WanIpSensor(hass, name, hostname, resolver, ipv6)], True) -class WanIpSensor(Entity): +class WanIpSensor(SensorEntity): """Implementation of a DNS IP sensor.""" def __init__(self, hass, name, hostname, resolver, ipv6): diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index fb2b6daecda..3e41c1871bf 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -221,7 +221,7 @@ class Doods(ImageProcessingEntity): return self._total_matches @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return { ATTR_MATCHES: self._matches, diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index f5d425cb9ef..6f6fcb0d6b3 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,6 +2,6 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==8.1.1"], + "requirements": ["pydoods==1.0.2", "pillow==8.1.2"], "codeowners": [] } diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 22db3c76273..3e8e59df203 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,11 +1,10 @@ """Support for DoorBird devices.""" import asyncio import logging -import urllib -from urllib.error import HTTPError from aiohttp import web from doorbirdpy import DoorBird +import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device = DoorBird(device_ip, username, password) try: status, info = await hass.async_add_executor_job(_init_doorbird_device, device) - except urllib.error.HTTPError as err: - if err.code == HTTP_UNAUTHORIZED: + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: _LOGGER.error( "Authorization rejected by DoorBird for %s@%s", username, device_ip ) @@ -168,9 +167,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -188,8 +187,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def _async_register_events(hass, doorstation): try: await hass.async_add_executor_job(doorstation.register_events, hass) - except HTTPError: + except requests.exceptions.HTTPError: hass.components.persistent_notification.async_create( "Doorbird configuration failed. Please verify that API " "Operator permission is enabled for the Doorbird user. " diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 4bbd7f8dc86..f69b38c7a7a 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,9 +1,9 @@ """Config flow for DoorBird integration.""" from ipaddress import ip_address import logging -import urllib from doorbirdpy import DoorBird +import requests import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -17,8 +17,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.util.network import is_link_local -from .const import CONF_EVENTS, DOORBIRD_OUI -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI from .util import get_mac_address_from_doorstation_info _LOGGER = logging.getLogger(__name__) @@ -35,17 +34,18 @@ def _schema_with_defaults(host=None, name=None): ) -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. +def _check_device(device): + """Verify we can connect to the device and return the status.""" + return device.ready(), device.info() - Data has the keys from DATA_SCHEMA with values provided by the user. - """ + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) try: - status = await hass.async_add_executor_job(device.ready) - info = await hass.async_add_executor_job(device.info) - except urllib.error.HTTPError as err: - if err.code == HTTP_UNAUTHORIZED: + status, info = await hass.async_add_executor_job(_check_device, device) + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: @@ -60,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": data[CONF_HOST], "mac_addr": mac_addr} +async def async_verify_supported_device(hass, host): + """Verify the doorbell state endpoint returns a 401.""" + device = DoorBird(host, "", "") + try: + await hass.async_add_executor_job(device.doorbell_state) + except requests.exceptions.HTTPError as err: + if err.response.status_code == HTTP_UNAUTHORIZED: + return True + except OSError: + return False + return False + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for DoorBird.""" @@ -86,17 +99,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered doorbird device.""" macaddress = discovery_info["properties"]["macaddress"] + host = discovery_info[CONF_HOST] if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") - if is_link_local(ip_address(discovery_info[CONF_HOST])): + if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") + if not await async_verify_supported_device(self.hass, host): + return self.async_abort(reason="not_doorbird_device") await self.async_set_unique_id(macaddress) - self._abort_if_unique_id_configured( - updates={CONF_HOST: discovery_info[CONF_HOST]} - ) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info["name"] @@ -105,11 +119,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, - CONF_HOST: discovery_info[CONF_HOST], + CONF_HOST: host, } - self.discovery_schema = _schema_with_defaults( - host=discovery_info[CONF_HOST], name=friendly_hostname - ) + self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname) return await self.async_step_user() diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index 618368433ac..3f74783b7ac 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -1,14 +1,19 @@ { "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" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { "host": "Hoszt", + "name": "Eszk\u00f6z neve", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/doorbird/translations/id.json b/homeassistant/components/doorbird/translations/id.json new file mode 100644 index 00000000000..f708780ce31 --- /dev/null +++ b/homeassistant/components/doorbird/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "link_local_address": "Tautan alamat lokal tidak didukung", + "not_doorbird_device": "Perangkat ini bukan DoorBird" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "DoorBird {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama Perangkat", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Daftar event yang dipisahkan koma." + }, + "description": "Tambahkan nama event yang dipisahkan koma untuk setiap event yang ingin dilacak. Setelah memasukkannya di sini, gunakan aplikasi DoorBird untuk menetapkannya ke event tertentu. Baca dokumentasi di https://www.home-assistant.io/integrations/doorbird/#events. Contoh: somebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/ko.json b/homeassistant/components/doorbird/translations/ko.json index 819b3b51d10..85d00317c2d 100644 --- a/homeassistant/components/doorbird/translations/ko.json +++ b/homeassistant/components/doorbird/translations/ko.json @@ -19,7 +19,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "DoorBird \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "DoorBird\uc5d0 \uc5f0\uacb0\ud558\uae30" } } }, @@ -29,7 +29,7 @@ "data": { "events": "\uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \ubaa9\ub85d." }, - "description": "\ucd94\uc801\ud558\ub824\ub294 \uac01 \uc774\ubca4\ud2b8\uc5d0 \ub300\ud574 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \uc774\ub984\uc744 \ucd94\uac00\ud574\uc8fc\uc138\uc694. \uc5ec\uae30\uc5d0 \uc785\ub825\ud55c \ud6c4 DoorBird \uc571\uc744 \uc0ac\uc6a9\ud558\uc5ec \ud2b9\uc815 \uc774\ubca4\ud2b8\uc5d0 \ud560\ub2f9\ud574\uc8fc\uc138\uc694. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 https://www.home-assistant.io/integrations/doorbird/#event \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc608: someone_pressed_the_button, motion" + "description": "\ucd94\uc801\ud558\ub824\ub294 \uac01 \uc774\ubca4\ud2b8\uc5d0 \ub300\ud574 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \uc774\ub984\uc744 \ucd94\uac00\ud574\uc8fc\uc138\uc694. \uc5ec\uae30\uc5d0 \uc785\ub825\ud55c \ud6c4 DoorBird \uc571\uc744 \uc0ac\uc6a9\ud558\uc5ec \ud2b9\uc815 \uc774\ubca4\ud2b8\uc5d0 \ud560\ub2f9\ud574\uc8fc\uc138\uc694. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 https://www.home-assistant.io/integrations/doorbird/#event \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc608: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json index 625367484b0..1c43ee2d9c2 100644 --- a/homeassistant/components/doorbird/translations/nl.json +++ b/homeassistant/components/doorbird/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Deze DoorBird is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "link_local_address": "Link-lokale adressen worden niet ondersteund", "not_doorbird_device": "Dit apparaat is geen DoorBird" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Host (IP-adres)", + "host": "Host", "name": "Apparaatnaam", "password": "Wachtwoord", "username": "Gebruikersnaam" diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index 5e376ee56d3..4d5695a3ab2 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -17,7 +17,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 DoorBird" } diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 46ff402788e..e7b3dbdd363 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -4,10 +4,9 @@ import re import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES, PERCENTAGE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DOMAIN as DOVADO_DOMAIN @@ -53,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities) -class DovadoSensor(Entity): +class DovadoSensor(SensorEntity): """Representation of a Dovado sensor.""" def __init__(self, data, sensor): @@ -105,6 +104,6 @@ class DovadoSensor(Entity): return SENSORS[self._sensor][2] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]} diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 3856df696ad..89aa4a465cf 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -80,7 +80,7 @@ def setup(hass, config): if req.status_code != HTTP_OK: _LOGGER.warning( - "downloading '%s' failed, status_code=%d", url, req.status_code + "Downloading '%s' failed, status_code=%d", url, req.status_code ) hass.bus.fire( f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 50823bb1d29..f130f500545 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,6 +1,7 @@ """The dsmr component.""" import asyncio from asyncio import CancelledError +from contextlib import suppress from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -8,11 +9,6 @@ from homeassistant.core import HomeAssistant from .const import DATA_LISTENER, DATA_TASK, DOMAIN, PLATFORMS -async def async_setup(hass, config: dict): - """Set up the DSMR platform.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up DSMR from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -36,16 +32,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): # Cancel the reconnect task task.cancel() - try: + with suppress(CancelledError): await task - except CancelledError: - pass unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index f0899598351..5a03a80ff52 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -1,8 +1,10 @@ """Config flow for DSMR integration.""" +from __future__ import annotations + import asyncio from functools import partial import logging -from typing import Any, Dict, Optional +from typing import Any from async_timeout import timeout from dsmr_parser import obis_references as obis_ref @@ -14,7 +16,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from .const import ( # pylint:disable=unused-import +from .const import ( CONF_DSMR_VERSION, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, @@ -133,7 +135,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, port: str, host: str = None, - updates: Optional[Dict[Any, Any]] = None, + updates: dict[Any, Any] | None = None, reload_on_update: bool = True, ): """Test if host and port are already configured.""" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index aea12a863f0..d17c3b780e4 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,17 +1,19 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" +from __future__ import annotations + import asyncio from asyncio import CancelledError +from contextlib import suppress from datetime import timedelta from functools import partial import logging -from typing import Dict from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader import serial import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -21,7 +23,6 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle @@ -285,7 +286,7 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task -class DSMREntity(Entity): +class DSMREntity(SensorEntity): """Entity reading values from DSMR telegram.""" def __init__(self, name, device_name, device_serial, obis, config, force_update): @@ -342,10 +343,8 @@ class DSMREntity(Entity): if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value, self._config[CONF_DSMR_VERSION]) - try: + with suppress(TypeError): value = round(float(value), self._config[CONF_PRECISION]) - except TypeError: - pass if value is not None: return value @@ -363,7 +362,7 @@ class DSMREntity(Entity): 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/fr.json b/homeassistant/components/dsmr/translations/fr.json index cb08a7865b3..d156aee8ca0 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, + "error": { + "one": "Vide", + "other": "Vide" + }, "step": { "one": "", "other": "Autre" diff --git a/homeassistant/components/dsmr/translations/id.json b/homeassistant/components/dsmr/translations/id.json new file mode 100644 index 00000000000..fd8299d61ed --- /dev/null +++ b/homeassistant/components/dsmr/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Interval minimum pembaruan entitas (dalam detik)" + }, + "title": "Opsi DSMR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/ko.json b/homeassistant/components/dsmr/translations/ko.json index 17dee71d640..73837e1b4f2 100644 --- a/homeassistant/components/dsmr/translations/ko.json +++ b/homeassistant/components/dsmr/translations/ko.json @@ -3,5 +3,15 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\uad6c\uc131\uc694\uc18c \uc5c6\ub370\uc774\ud2b8 \uac04 \ucd5c\uc18c \uc2dc\uac04 (\ucd08)" + }, + "title": "DSMR \uc635\uc158" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 309f0d297ec..daf6b9eb950 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -325,4 +325,160 @@ DEFINITIONS = { "enable_default": True, "icon": "mdi:flash", }, + "dsmr/current-month/electricity1": { + "name": "Current month low tariff usage", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-month/electricity2": { + "name": "Current month high tariff usage", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-month/electricity1_returned": { + "name": "Current month low tariff returned", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-month/electricity2_returned": { + "name": "Current month high tariff returned", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-month/electricity_merged": { + "name": "Current month power usage total", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-month/electricity_returned_merged": { + "name": "Current month power return total", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-month/electricity1_cost": { + "name": "Current month low tariff cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-month/electricity2_cost": { + "name": "Current month high tariff cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-month/electricity_cost_merged": { + "name": "Current month power total cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-month/gas": { + "name": "Current month gas usage", + "enable_default": True, + "icon": "mdi:counter", + "unit": VOLUME_CUBIC_METERS, + }, + "dsmr/current-month/gas_cost": { + "name": "Current month gas cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-month/fixed_cost": { + "name": "Current month fixed cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-month/total_cost": { + "name": "Current month total cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-year/electricity1": { + "name": "Current year low tariff usage", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-year/electricity2": { + "name": "Current year high tariff usage", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-year/electricity1_returned": { + "name": "Current year low tariff returned", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-year/electricity2_returned": { + "name": "Current year high tariff usage", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-year/electricity_merged": { + "name": "Current year power usage total", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-year/electricity_returned_merged": { + "name": "Current year power returned total", + "enable_default": True, + "device_class": DEVICE_CLASS_ENERGY, + "unit": ENERGY_KILO_WATT_HOUR, + }, + "dsmr/current-year/electricity1_cost": { + "name": "Current year low tariff cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-year/electricity2_cost": { + "name": "Current year high tariff cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-year/electricity_cost_merged": { + "name": "Current year power total cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-year/gas": { + "name": "Current year gas usage", + "enable_default": True, + "icon": "mdi:counter", + "unit": VOLUME_CUBIC_METERS, + }, + "dsmr/current-year/gas_cost": { + "name": "Current year gas cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-year/fixed_cost": { + "name": "Current year fixed cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, + "dsmr/current-year/total_cost": { + "name": "Current year total cost", + "enable_default": True, + "icon": "mdi:currency-eur", + "unit": CURRENCY_EURO, + }, } diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 14234b49dbe..0ee5932c1bb 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -1,7 +1,7 @@ """Support for DSMR Reader through MQTT.""" from homeassistant.components import mqtt +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from .definitions import DEFINITIONS @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class DSMRSensor(Entity): +class DSMRSensor(SensorEntity): """Representation of a DSMR sensor that is updated via MQTT.""" def __init__(self, topic): diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index efd00b3da1e..27475990de0 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -4,10 +4,9 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, HTTP_OK import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -39,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([DteEnergyBridgeSensor(ip_address, name, version)], True) -class DteEnergyBridgeSensor(Entity): +class DteEnergyBridgeSensor(SensorEntity): """Implementation of the DTE Energy Bridge sensors.""" def __init__(self, ip_address, name, version): diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 96e485a94e6..dbe1d10b553 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -4,15 +4,15 @@ Support for Dublin RTPI information from data.dublinked.ie. For more info on the API see : https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus-bus-eireann-luas-and-irish-rail/resource/4b9f2c4f-6bf5-4958-a43a-f12dab04cf61 """ +from contextlib import suppress from datetime import datetime, timedelta import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, HTTP_OK, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _RESOURCE = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation" @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([DublinPublicTransportSensor(data, stop, route, name)], True) -class DublinPublicTransportSensor(Entity): +class DublinPublicTransportSensor(SensorEntity): """Implementation of an Dublin public transport sensor.""" def __init__(self, data, stop, route, name): @@ -87,7 +87,7 @@ class DublinPublicTransportSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._times is not None: next_up = "None" @@ -118,10 +118,8 @@ class DublinPublicTransportSensor(Entity): """Get the latest data from opendata.ch and update the states.""" self.data.update() self._times = self.data.info - try: + with suppress(TypeError): self._state = self._times[0][ATTR_DUE_IN] - except TypeError: - pass class PublicTransportData: diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 3e08d0e7b34..76353415d4f 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -103,7 +103,7 @@ async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False) def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" if not iscoroutinefunction: - _LOGGER.error("action needs to be a coroutine and return True/False") + _LOGGER.error("Action needs to be a coroutine and return True/False") return if not isinstance(intervals, (list, tuple)): diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index a1fa456aa09..10c66c3bfb0 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -10,11 +10,6 @@ from .const import DOMAIN PLATFORMS = ["media_player"] -async def async_setup(hass, config): - """Set up the Dune HD component.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up a config entry.""" host = config_entry.data[CONF_HOST] @@ -24,9 +19,9 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = player - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -37,8 +32,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 1f0efa668f9..998ff408f36 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json index 44b4442dc31..cf0b593d546 100644 --- a/homeassistant/components/dunehd/translations/hu.json +++ b/homeassistant/components/dunehd/translations/hu.json @@ -4,7 +4,17 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + }, + "title": "Dune HD" + } } } } \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/id.json b/homeassistant/components/dunehd/translations/id.json new file mode 100644 index 00000000000..25cb96bedea --- /dev/null +++ b/homeassistant/components/dunehd/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Siapkan integrasi Dune HD. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/dunehd \n\nPastikan pemutar Anda dinyalakan.", + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/ko.json b/homeassistant/components/dunehd/translations/ko.json index 45a59b4d75b..5c7feb27f7e 100644 --- a/homeassistant/components/dunehd/translations/ko.json +++ b/homeassistant/components/dunehd/translations/ko.json @@ -6,7 +6,7 @@ "error": { "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", - "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \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" }, "step": { "user": { diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json index c8e16770db2..bb3dd7def47 100644 --- a/homeassistant/components/dunehd/translations/nl.json +++ b/homeassistant/components/dunehd/translations/nl.json @@ -12,7 +12,9 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Stel Dune HD integratie in. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/dunehd \n\nZorg ervoor dat uw speler is ingeschakeld.", + "title": "Dune HD" } } } diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 79beebb005d..78fa9bd8552 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -15,10 +15,9 @@ import logging from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -87,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class DwdWeatherWarningsSensor(Entity): +class DwdWeatherWarningsSensor(SensorEntity): """Representation of a DWD-Weather-Warnings sensor.""" def __init__(self, api, name, sensor_type): @@ -119,7 +118,7 @@ class DwdWeatherWarningsSensor(Entity): return self._api.api.expected_warning_level @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" data = { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index f3f604ff369..f1243cd5407 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -6,7 +6,7 @@ import logging import dweepy import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([DweetSensor(hass, dweet, name, value_template, unit)], True) -class DweetSensor(Entity): +class DweetSensor(SensorEntity): """Representation of a Dweet sensor.""" def __init__(self, hass, dweet, name, value_template, unit_of_measurement): diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index e52ec5946b6..92392e4b51a 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,7 +1,8 @@ """Support for the Dynalite networks.""" +from __future__ import annotations import asyncio -from typing import Any, Dict, Union +from typing import Any import voluptuous as vol @@ -47,14 +48,14 @@ from .const import ( DEFAULT_PORT, DEFAULT_TEMPLATES, DOMAIN, - ENTITY_PLATFORMS, LOGGER, + PLATFORMS, SERVICE_REQUEST_AREA_PRESET, SERVICE_REQUEST_CHANNEL_LEVEL, ) -def num_string(value: Union[int, str]) -> str: +def num_string(value: int | str) -> str: """Test if value is a string of digits, aka an integer.""" new_value = str(value) if new_value.isdigit(): @@ -105,7 +106,7 @@ TEMPLATE_DATA_SCHEMA = vol.Any(TEMPLATE_ROOM_SCHEMA, TEMPLATE_TIMECOVER_SCHEMA) TEMPLATE_SCHEMA = vol.Schema({str: TEMPLATE_DATA_SCHEMA}) -def validate_area(config: Dict[str, Any]) -> Dict[str, Any]: +def validate_area(config: dict[str, Any]) -> dict[str, Any]: """Validate that template parameters are only used if area is using the relevant template.""" conf_set = set() for template in DEFAULT_TEMPLATES: @@ -178,7 +179,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Set up the Dynalite platform.""" conf = config.get(DOMAIN) LOGGER.debug("Setting up dynalite component config = %s", conf) @@ -267,14 +268,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # need to do it before the listener hass.data[DOMAIN][entry.entry_id] = bridge 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 ENTITY_PLATFORMS: + + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) + return True @@ -284,7 +288,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) tasks = [ hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in ENTITY_PLATFORMS + for platform in PLATFORMS ] results = await asyncio.gather(*tasks) return False not in results diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 5bf21801b3d..71cecee8d43 100644 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -1,6 +1,7 @@ """Code to handle a Dynalite bridge.""" +from __future__ import annotations -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable from dynalite_devices_lib.dynalite_devices import ( CONF_AREA as dyn_CONF_AREA, @@ -16,21 +17,14 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ( - ATTR_AREA, - ATTR_HOST, - ATTR_PACKET, - ATTR_PRESET, - ENTITY_PLATFORMS, - LOGGER, -) +from .const import ATTR_AREA, ATTR_HOST, ATTR_PACKET, ATTR_PRESET, LOGGER, PLATFORMS from .convert_config import convert_config class DynaliteBridge: """Manages a single Dynalite bridge.""" - def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: + def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the system based on host parameter.""" self.hass = hass self.area = {} @@ -51,12 +45,12 @@ class DynaliteBridge: LOGGER.debug("Setting up bridge - host %s", self.host) return await self.dynalite_devices.async_setup() - def reload_config(self, config: Dict[str, Any]) -> None: + def reload_config(self, config: dict[str, Any]) -> None: """Reconfigure a bridge when config changes.""" LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config) self.dynalite_devices.configure(convert_config(config)) - def update_signal(self, device: Optional[DynaliteBaseDevice] = None) -> str: + def update_signal(self, device: DynaliteBaseDevice | None = None) -> str: """Create signal to use to trigger entity update.""" if device: signal = f"dynalite-update-{self.host}-{device.unique_id}" @@ -65,7 +59,7 @@ class DynaliteBridge: return signal @callback - def update_device(self, device: Optional[DynaliteBaseDevice] = None) -> None: + def update_device(self, device: DynaliteBaseDevice | None = None) -> None: """Call when a device or all devices should be updated.""" if not device: # This is used to signal connection or disconnection, so all devices may become available or not. @@ -105,9 +99,9 @@ class DynaliteBridge: if platform in self.waiting_devices: self.async_add_devices[platform](self.waiting_devices[platform]) - def add_devices_when_registered(self, devices: List[DynaliteBaseDevice]) -> None: + def add_devices_when_registered(self, devices: list[DynaliteBaseDevice]) -> None: """Add the devices to HA if the add devices callback was registered, otherwise queue until it is.""" - for platform in ENTITY_PLATFORMS: + for platform in PLATFORMS: platform_devices = [ device for device in devices if device.category == platform ] diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 4c5b2ceb7d8..2bd2b142252 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure Dynalite hub.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any from homeassistant import config_entries from homeassistant.const import CONF_HOST @@ -18,7 +20,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the Dynalite flow.""" self.host = None - async def async_step_import(self, import_info: Dict[str, Any]) -> Any: + async def async_step_import(self, import_info: dict[str, Any]) -> Any: """Import a new bridge as a config entry.""" LOGGER.debug("Starting async_step_import - %s", import_info) host = import_info[CONF_HOST] diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index 3f1e201f3fd..eda43305461 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_ROOM LOGGER = logging.getLogger(__package__) DOMAIN = "dynalite" -ENTITY_PLATFORMS = ["light", "switch", "cover"] +PLATFORMS = ["light", "switch", "cover"] CONF_ACTIVE = "active" diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index 6a85147a2e0..89a7f32b47a 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -1,6 +1,7 @@ """Convert the HA config to the dynalite config.""" +from __future__ import annotations -from typing import Any, Dict +from typing import Any from dynalite_devices_lib import const as dyn_const @@ -62,7 +63,7 @@ def convert_with_map(config, conf_map): return result -def convert_channel(config: Dict[str, Any]) -> Dict[str, Any]: +def convert_channel(config: dict[str, Any]) -> dict[str, Any]: """Convert the config for a channel.""" my_map = { CONF_NAME: dyn_const.CONF_NAME, @@ -72,7 +73,7 @@ def convert_channel(config: Dict[str, Any]) -> Dict[str, Any]: return convert_with_map(config, my_map) -def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]: +def convert_preset(config: dict[str, Any]) -> dict[str, Any]: """Convert the config for a preset.""" my_map = { CONF_NAME: dyn_const.CONF_NAME, @@ -82,7 +83,7 @@ def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]: return convert_with_map(config, my_map) -def convert_area(config: Dict[str, Any]) -> Dict[str, Any]: +def convert_area(config: dict[str, Any]) -> dict[str, Any]: """Convert the config for an area.""" my_map = { CONF_NAME: dyn_const.CONF_NAME, @@ -114,12 +115,12 @@ def convert_area(config: Dict[str, Any]) -> Dict[str, Any]: return result -def convert_default(config: Dict[str, Any]) -> Dict[str, Any]: +def convert_default(config: dict[str, Any]) -> dict[str, Any]: """Convert the config for the platform defaults.""" return convert_with_map(config, {CONF_FADE: dyn_const.CONF_FADE}) -def convert_template(config: Dict[str, Any]) -> Dict[str, Any]: +def convert_template(config: dict[str, Any]) -> dict[str, Any]: """Convert the config for a template.""" my_map = { CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, @@ -135,7 +136,7 @@ def convert_template(config: Dict[str, Any]) -> Dict[str, Any]: return convert_with_map(config, my_map) -def convert_config(config: Dict[str, Any]) -> Dict[str, Any]: +def convert_config(config: dict[str, Any]) -> dict[str, Any]: """Convert a config dict by replacing component consts with library consts.""" my_map = { CONF_NAME: dyn_const.CONF_NAME, diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 31879c5c118..2cc28002a2c 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -1,5 +1,7 @@ """Support for the Dynalite devices as entities.""" -from typing import Any, Callable, Dict +from __future__ import annotations + +from typing import Any, Callable from homeassistant.components.dynalite.bridge import DynaliteBridge from homeassistant.config_entries import ConfigEntry @@ -58,7 +60,7 @@ class DynaliteBase(Entity): return self._device.available @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Device info for this entity.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index b39af2a2fd1..d8023b42973 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -17,7 +17,7 @@ CONF_RETRY = "retry" DEFAULT_TIMEOUT = 5 DEFAULT_RETRY = 10 DYSON_DEVICES = "dyson_devices" -DYSON_PLATFORMS = ["sensor", "fan", "vacuum", "climate", "air_quality"] +PLATFORMS = ["sensor", "fan", "vacuum", "climate", "air_quality"] DOMAIN = "dyson" @@ -105,7 +105,7 @@ def setup(hass, config): # Start fan/sensors components if hass.data[DYSON_DEVICES]: _LOGGER.debug("Starting sensor/fan components") - for platform in DYSON_PLATFORMS: + for platform in PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index 3bf4f2bb34c..48b66fe7683 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -88,6 +88,6 @@ class DysonAirSensor(DysonEntity, AirQualityEntity): return int(self._device.environmental_state.volatile_organic_compounds) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return {ATTR_VOC: self.volatile_organic_compounds} diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index a8e737bb48b..e646babb944 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -1,7 +1,8 @@ """Support for Dyson Pure Cool link fan.""" +from __future__ import annotations + import logging import math -from typing import Optional from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool @@ -200,7 +201,7 @@ class DysonFanEntity(DysonEntity, FanEntity): return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return optional state attributes.""" return { ATTR_NIGHT_MODE: self.night_mode, @@ -243,9 +244,9 @@ class DysonFanEntity(DysonEntity, FanEntity): def turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" @@ -325,9 +326,9 @@ class DysonPureCoolEntity(DysonFanEntity): def turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" @@ -455,10 +456,10 @@ class DysonPureCoolEntity(DysonFanEntity): return int(self._device.state.carbon_filter_state) @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return optional state attributes.""" return { - **super().device_state_attributes, + **super().extra_state_attributes, ATTR_ANGLE_LOW: self.angle_low, ATTR_ANGLE_HIGH: self.angle_high, ATTR_FLOW_DIRECTION_FRONT: self.flow_direction_front, diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 80a64e787f0..cff4b8f5501 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -2,6 +2,7 @@ from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -13,7 +14,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_HOURS, ) -from homeassistant.helpers.entity import Entity from . import DYSON_DEVICES, DysonEntity @@ -101,7 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class DysonSensor(DysonEntity, Entity): +class DysonSensor(DysonEntity, SensorEntity): """Representation of a generic Dyson sensor.""" def __init__(self, device, sensor_type): diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index 466b409c342..f4035d33cf3 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -95,7 +95,7 @@ class Dyson360EyeDevice(DysonEntity, VacuumEntity): return ["Quiet", "Max"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the specific state attributes of this vacuum cleaner.""" return {ATTR_POSITION: str(self._device.state.position)} diff --git a/homeassistant/components/eafm/config_flow.py b/homeassistant/components/eafm/config_flow.py index 19b10c3e6c5..98e9158e2df 100644 --- a/homeassistant/components/eafm/config_flow.py +++ b/homeassistant/components/eafm/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.helpers.aiohttp_client import async_get_clientsession -# pylint: disable=unused-import from .const import DOMAIN diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index 746f6f34abc..b3d726f9cd3 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -5,6 +5,7 @@ import logging from aioeafm import get_station import async_timeout +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import ( @@ -77,7 +78,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await coordinator.async_refresh() -class Measurement(CoordinatorEntity): +class Measurement(CoordinatorEntity, SensorEntity): """A gauge at a flood monitoring station.""" attribution = "This uses Environment Agency flood and river level data from the real-time data API" @@ -156,7 +157,7 @@ class Measurement(CoordinatorEntity): return UNIT_MAPPING.get(measure["unit"], measure["unitName"]) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the sensor specific state attributes.""" return {ATTR_ATTRIBUTION: self.attribution} diff --git a/homeassistant/components/eafm/translations/id.json b/homeassistant/components/eafm/translations/id.json new file mode 100644 index 00000000000..656c9eb51ea --- /dev/null +++ b/homeassistant/components/eafm/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_stations": "Tidak ditemukan stasiun pemantau banjir." + }, + "step": { + "user": { + "data": { + "station": "Stasiun" + }, + "description": "Pilih stasiun yang ingin dipantau", + "title": "Lacak stasiun pemantauan banjir" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/ko.json b/homeassistant/components/eafm/translations/ko.json index 36af97756ee..5c6c0a8aa33 100644 --- a/homeassistant/components/eafm/translations/ko.json +++ b/homeassistant/components/eafm/translations/ko.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." + "no_stations": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { "user": { "data": { "station": "\uc2a4\ud14c\uc774\uc158" }, - "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc2a4\ud14c\uc774\uc158 \uc120\ud0dd", - "title": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158 \ucd94\uc801" + "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc2a4\ud14c\uc774\uc158 \uc120\ud0dd\ud558\uae30", + "title": "\ud64d\uc218 \ubaa8\ub2c8\ud130\ub9c1 \uc2a4\ud14c\uc774\uc158 \ucd94\uc801\ud558\uae30" } } } diff --git a/homeassistant/components/eafm/translations/nl.json b/homeassistant/components/eafm/translations/nl.json index 8b2702b6708..ed67ed8f982 100644 --- a/homeassistant/components/eafm/translations/nl.json +++ b/homeassistant/components/eafm/translations/nl.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "no_stations": "Geen meetstations voor overstromingen gevonden." + }, + "step": { + "user": { + "data": { + "station": "Station" + }, + "description": "Selecteer het station dat u wilt monitoren", + "title": "Volg een station voor overstromingen" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index aa6945bac99..72d169f389e 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -10,7 +10,7 @@ from pyebox import EboxClient from pyebox.client import PyEboxError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -22,7 +22,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -90,7 +89,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, True) -class EBoxSensor(Entity): +class EBoxSensor(SensorEntity): """Implementation of a EBox sensor.""" def __init__(self, ebox_data, sensor_type, name): diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index 00c40344d6e..beb8abd6289 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -115,8 +115,7 @@ class EbusdData: try: _LOGGER.debug("Opening socket to ebusd %s", name) command_result = ebusdpy.write(self._address, self._circuit, name, value) - if command_result is not None: - if "done" not in command_result: - _LOGGER.warning("Write command failed: %s", name) + if command_result is not None and "done" not in command_result: + _LOGGER.warning("Write command failed: %s", name) except RuntimeError as err: _LOGGER.error(err) diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index badb94a6f85..00f6a6b2b3e 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -2,7 +2,7 @@ import datetime import logging -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -34,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class EbusdSensor(Entity): +class EbusdSensor(SensorEntity): """Ebusd component sensor methods definition.""" def __init__(self, data, sensor, name): @@ -55,7 +55,7 @@ class EbusdSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if self._type == 1 and self._state is not None: schedule = { diff --git a/homeassistant/components/ebusd/translations/id.json b/homeassistant/components/ebusd/translations/id.json new file mode 100644 index 00000000000..6b2aaa6e789 --- /dev/null +++ b/homeassistant/components/ebusd/translations/id.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Siang", + "night": "Malam" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index 963f547283f..e1c9308b5a9 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -1,6 +1,6 @@ """Allows reading temperatures from ecoal/esterownik.pl controller.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import TEMP_CELSIUS -from homeassistant.helpers.entity import Entity from . import AVAILABLE_SENSORS, DATA_ECOAL_BOILER @@ -17,7 +17,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class EcoalTempSensor(Entity): +class EcoalTempSensor(SensorEntity): """Representation of a temperature sensor using ecoal status data.""" def __init__(self, ecoal_contr, name, status_attr): diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index 57a8d420c43..995a49554e6 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -1,5 +1,5 @@ """Allows to configuration ecoal (esterownik.pl) pumps as switches.""" -from typing import Optional +from __future__ import annotations from homeassistant.components.switch import SwitchEntity @@ -40,7 +40,7 @@ class EcoalSwitch(SwitchEntity): self._state = None @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the switch.""" return self._name diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 26bfbe5b3da..015ee1fbf6c 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -10,13 +10,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -from .const import ( - _LOGGER, - CONF_REFRESH_TOKEN, - DATA_ECOBEE_CONFIG, - DOMAIN, - ECOBEE_PLATFORMS, -) +from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, PLATFORMS MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) @@ -32,7 +26,7 @@ async def async_setup(hass, config): But, an "ecobee:" entry in configuration.yaml will trigger an import flow if a config entry doesn't already exist. If ecobee.conf exists, the import flow will attempt to import it and create a config entry, to assist users - migrating from the old ecobee component. Otherwise, the user will have to + migrating from the old ecobee integration. Otherwise, the user will have to continue setting up the integration via the config flow. """ hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {}) @@ -66,9 +60,9 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN] = data - for component in ECOBEE_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -120,7 +114,7 @@ async def async_unload_entry(hass, config_entry): hass.data.pop(DOMAIN) tasks = [] - for platform in ECOBEE_PLATFORMS: + for platform in PLATFORMS: tasks.append( hass.config_entries.async_forward_entry_unload(config_entry, platform) ) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 089f0950854..47c2ff969ec 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -1,6 +1,7 @@ """Support for Ecobee Thermostats.""" +from __future__ import annotations + import collections -from typing import Optional import voluptuous as vol @@ -406,7 +407,7 @@ class Thermostat(ClimateEntity): ) @property - def target_humidity(self) -> Optional[int]: + def target_humidity(self) -> int | None: """Return the desired humidity set point.""" if self.has_humidifier_control: return self.thermostat["runtime"]["desiredHumidity"] @@ -484,7 +485,7 @@ class Thermostat(ClimateEntity): return self._operation_list @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> int | None: """Return the current humidity.""" return self.thermostat["runtime"]["actualHumidity"] @@ -519,7 +520,7 @@ class Thermostat(ClimateEntity): return CURRENT_HVAC_IDLE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" status = self.thermostat["equipmentStatus"] return { diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 5ec3a0fcf96..44abafe8380 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -37,7 +37,7 @@ ECOBEE_MODEL_TO_NAME = { "vulcanSmart": "ecobee4 Smart", } -ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] +PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] MANUFACTURER = "ecobee" diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index cfbaa7a4516..5abe809e59d 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,13 +1,13 @@ """Support for Ecobee sensors.""" from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(dev, True) -class EcobeeSensor(Entity): +class EcobeeSensor(SensorEntity): """Representation of an Ecobee sensor.""" def __init__(self, data, sensor_name, sensor_type, sensor_index): diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 78f0708134c..19f379de7d9 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -10,7 +10,7 @@ }, "authorize": { "title": "Authorize app on ecobee.com", - "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with PIN code:\n\n{pin}\n\nThen, press Submit." } }, "error": { diff --git a/homeassistant/components/ecobee/translations/ca.json b/homeassistant/components/ecobee/translations/ca.json index 46d42d0774b..99b3f234df2 100644 --- a/homeassistant/components/ecobee/translations/ca.json +++ b/homeassistant/components/ecobee/translations/ca.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi pin seg\u00fcent: \n\n {pin} \n \n A continuaci\u00f3, prem Enviar.", + "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi PIN: \n\n {pin} \n \n A continuaci\u00f3, prem Envia.", "title": "Autoritzaci\u00f3 de l'aplicaci\u00f3 a ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/translations/en.json b/homeassistant/components/ecobee/translations/en.json index 1dfcc2f6a19..8a8beeb63c4 100644 --- a/homeassistant/components/ecobee/translations/en.json +++ b/homeassistant/components/ecobee/translations/en.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit.", + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with PIN code:\n\n{pin}\n\nThen, press Submit.", "title": "Authorize app on ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/translations/et.json b/homeassistant/components/ecobee/translations/et.json index 46c332a5356..452cbd578fa 100644 --- a/homeassistant/components/ecobee/translations/et.json +++ b/homeassistant/components/ecobee/translations/et.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Tuvasta see rakendus aadressil https://www.ecobee.com/consumerportal/index.html koos PIN-koodiga:\n\n {pin}\n\n Seej\u00e4rel vajuta Esita.", + "description": "kinnita see rakendus aadressil https://www.ecobee.com/consumerportal/index.html PIN koodiga:\n\n {pin}\n\n Seej\u00e4rel vajuta Esita.", "title": "Rakenduse tuvastamine saidil ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/translations/fr.json b/homeassistant/components/ecobee/translations/fr.json index cfb307053da..acbc909d881 100644 --- a/homeassistant/components/ecobee/translations/fr.json +++ b/homeassistant/components/ecobee/translations/fr.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec un code PIN :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.", + "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec le code NIP :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.", "title": "Autoriser l'application sur ecobee.com" }, "user": { diff --git a/homeassistant/components/ecobee/translations/hu.json b/homeassistant/components/ecobee/translations/hu.json index bd620bc9685..a91478ff038 100644 --- a/homeassistant/components/ecobee/translations/hu.json +++ b/homeassistant/components/ecobee/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "error": { "pin_request_failed": "Hiba t\u00f6rt\u00e9nt a PIN-k\u00f3d ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 k\u00e9r\u00e9sekor; ellen\u0151rizze, hogy az API-kulcs helyes-e.", "token_request_failed": "Hiba t\u00f6rt\u00e9nt a tokenek ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 ig\u00e9nyl\u00e9se k\u00f6zben; pr\u00f3b\u00e1lkozzon \u00fajra." diff --git a/homeassistant/components/ecobee/translations/id.json b/homeassistant/components/ecobee/translations/id.json new file mode 100644 index 00000000000..7d23b0ca141 --- /dev/null +++ b/homeassistant/components/ecobee/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "pin_request_failed": "Terjadi kesalahan saat meminta PIN dari ecobee. Verifikasi apakah kunci API sudah benar.", + "token_request_failed": "Kesalahan saat meminta token dari ecobee. Coba lagi" + }, + "step": { + "authorize": { + "description": "Otorisasi aplikasi ini di https://www.ecobee.com/consumerportal/index.html dengan kode PIN:\n\n{pin}\n\nKemudian, tekan Kirim.", + "title": "Otorisasi aplikasi di ecobee.com" + }, + "user": { + "data": { + "api_key": "Kunci API" + }, + "description": "Masukkan kunci API yang diperoleh dari ecobee.com.", + "title": "Kunci API ecobee" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/ko.json b/homeassistant/components/ecobee/translations/ko.json index 674b087620a..45406df54b6 100644 --- a/homeassistant/components/ecobee/translations/ko.json +++ b/homeassistant/components/ecobee/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/ecobee/translations/no.json b/homeassistant/components/ecobee/translations/no.json index f3c2eceee44..0492ff76cc6 100644 --- a/homeassistant/components/ecobee/translations/no.json +++ b/homeassistant/components/ecobee/translations/no.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Vennligst godkjenn denne appen p\u00e5 [https://www.ecobee.com/consumerportal](https://www.ecobee.com/consumerportal) med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 send.", + "description": "Autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med PIN-kode: \n\n {pin}\n\n Trykk deretter p\u00e5 Send.", "title": "Godkjenn app p\u00e5 ecobee.com" }, "user": { diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index dce4550eb1b..e605b16a237 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -13,7 +13,7 @@ from pyeconet.errors import ( PyeconetError, ) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send @@ -24,7 +24,7 @@ from .const import API_CLIENT, DOMAIN, EQUIPMENT _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor", "water_heater"] +PLATFORMS = ["climate", "binary_sensor", "sensor", "water_heater"] PUSH_UPDATE = "econet.push_update" INTERVAL = timedelta(minutes=60) @@ -54,15 +54,17 @@ async def async_setup_entry(hass, config_entry): raise ConfigEntryNotReady from err try: - equipment = await api.get_equipment_by_type([EquipmentType.WATER_HEATER]) + equipment = await api.get_equipment_by_type( + [EquipmentType.WATER_HEATER, EquipmentType.THERMOSTAT] + ) except (ClientError, GenericHTTPError, InvalidResponseFormat) as err: raise ConfigEntryNotReady from err hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) api.subscribe() @@ -74,6 +76,9 @@ async def async_setup_entry(hass, config_entry): for _eqip in equipment[EquipmentType.WATER_HEATER]: _eqip.set_update_callback(update_published) + for _eqip in equipment[EquipmentType.THERMOSTAT]: + _eqip.set_update_callback(update_published) + async def resubscribe(now): """Resubscribe to the MQTT updates.""" await hass.async_add_executor_job(api.unsubscribe) @@ -92,8 +97,8 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, entry): """Unload a EcoNet config entry.""" tasks = [ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] await asyncio.gather(*tasks) @@ -149,6 +154,11 @@ class EcoNetEntity(Entity): """Return the unique ID of the entity.""" return f"{self._econet.device_id}_{self._econet.device_name}" + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py index b87e6bb0cd0..116b1243ee0 100644 --- a/homeassistant/components/econet/binary_sensor.py +++ b/homeassistant/components/econet/binary_sensor.py @@ -2,8 +2,10 @@ from pyeconet.equipment import EquipmentType from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_LOCK, DEVICE_CLASS_OPENING, DEVICE_CLASS_POWER, + DEVICE_CLASS_SOUND, BinarySensorEntity, ) @@ -12,27 +14,40 @@ from .const import DOMAIN, EQUIPMENT SENSOR_NAME_RUNNING = "running" SENSOR_NAME_SHUTOFF_VALVE = "shutoff_valve" -SENSOR_NAME_VACATION = "vacation" +SENSOR_NAME_RUNNING = "running" +SENSOR_NAME_SCREEN_LOCKED = "screen_locked" +SENSOR_NAME_BEEP_ENABLED = "beep_enabled" + +ATTR = "attr" +DEVICE_CLASS = "device_class" +SENSORS = { + SENSOR_NAME_SHUTOFF_VALVE: { + ATTR: "shutoff_valve_open", + DEVICE_CLASS: DEVICE_CLASS_OPENING, + }, + SENSOR_NAME_RUNNING: {ATTR: "running", DEVICE_CLASS: DEVICE_CLASS_POWER}, + SENSOR_NAME_SCREEN_LOCKED: { + ATTR: "screen_locked", + DEVICE_CLASS: DEVICE_CLASS_LOCK, + }, + SENSOR_NAME_BEEP_ENABLED: { + ATTR: "beep_enabled", + DEVICE_CLASS: DEVICE_CLASS_SOUND, + }, +} async def async_setup_entry(hass, entry, async_add_entities): """Set up EcoNet binary sensor based on a config entry.""" equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] binary_sensors = [] - for water_heater in equipment[EquipmentType.WATER_HEATER]: - if water_heater.has_shutoff_valve: - binary_sensors.append( - EcoNetBinarySensor( - water_heater, - SENSOR_NAME_SHUTOFF_VALVE, - ) - ) - if water_heater.running is not None: - binary_sensors.append(EcoNetBinarySensor(water_heater, SENSOR_NAME_RUNNING)) - if water_heater.vacation is not None: - binary_sensors.append( - EcoNetBinarySensor(water_heater, SENSOR_NAME_VACATION) - ) + all_equipment = equipment[EquipmentType.WATER_HEATER].copy() + all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy()) + for _equip in all_equipment: + for sensor_name, sensor in SENSORS.items(): + if getattr(_equip, sensor[ATTR], None) is not None: + binary_sensors.append(EcoNetBinarySensor(_equip, sensor_name)) + async_add_entities(binary_sensors) @@ -48,22 +63,12 @@ class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity): @property def is_on(self): """Return true if the binary sensor is on.""" - if self._device_name == SENSOR_NAME_SHUTOFF_VALVE: - return self._econet.shutoff_valve_open - if self._device_name == SENSOR_NAME_RUNNING: - return self._econet.running - if self._device_name == SENSOR_NAME_VACATION: - return self._econet.vacation - return False + return getattr(self._econet, SENSORS[self._device_name][ATTR]) @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - if self._device_name == SENSOR_NAME_SHUTOFF_VALVE: - return DEVICE_CLASS_OPENING - if self._device_name == SENSOR_NAME_RUNNING: - return DEVICE_CLASS_POWER - return None + return SENSORS[self._device_name][DEVICE_CLASS] @property def name(self): diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py new file mode 100644 index 00000000000..fe50855d559 --- /dev/null +++ b/homeassistant/components/econet/climate.py @@ -0,0 +1,241 @@ +"""Support for Rheem EcoNet thermostats.""" +import logging + +from pyeconet.equipment import EquipmentType +from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import ATTR_TEMPERATURE + +from . import EcoNetEntity +from .const import DOMAIN, EQUIPMENT + +_LOGGER = logging.getLogger(__name__) + +ECONET_STATE_TO_HA = { + ThermostatOperationMode.HEATING: HVAC_MODE_HEAT, + ThermostatOperationMode.COOLING: HVAC_MODE_COOL, + ThermostatOperationMode.OFF: HVAC_MODE_OFF, + ThermostatOperationMode.AUTO: HVAC_MODE_HEAT_COOL, + ThermostatOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, +} +HA_STATE_TO_ECONET = {value: key for key, value in ECONET_STATE_TO_HA.items()} + +ECONET_FAN_STATE_TO_HA = { + ThermostatFanMode.AUTO: FAN_AUTO, + ThermostatFanMode.LOW: FAN_LOW, + ThermostatFanMode.MEDIUM: FAN_MEDIUM, + ThermostatFanMode.HIGH: FAN_HIGH, +} +HA_FAN_STATE_TO_ECONET = {value: key for key, value in ECONET_FAN_STATE_TO_HA.items()} + +SUPPORT_FLAGS_THERMOSTAT = ( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_AUX_HEAT +) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up EcoNet thermostat based on a config entry.""" + equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] + async_add_entities( + [ + EcoNetThermostat(thermostat) + for thermostat in equipment[EquipmentType.THERMOSTAT] + ], + ) + + +class EcoNetThermostat(EcoNetEntity, ClimateEntity): + """Define a Econet thermostat.""" + + def __init__(self, thermostat): + """Initialize.""" + super().__init__(thermostat) + self._running = thermostat.running + self._poll = True + self.econet_state_to_ha = {} + self.ha_state_to_econet = {} + self.op_list = [] + for mode in self._econet.modes: + if mode not in [ + ThermostatOperationMode.UNKNOWN, + ThermostatOperationMode.EMERGENCY_HEAT, + ]: + ha_mode = ECONET_STATE_TO_HA[mode] + self.op_list.append(ha_mode) + + @property + def supported_features(self): + """Return the list of supported features.""" + if self._econet.supports_humidifier: + return SUPPORT_FLAGS_THERMOSTAT | SUPPORT_TARGET_HUMIDITY + return SUPPORT_FLAGS_THERMOSTAT + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._econet.set_point + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._econet.humidity + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + if self._econet.supports_humidifier: + return self._econet.dehumidifier_set_point + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.hvac_mode == HVAC_MODE_COOL: + return self._econet.cool_set_point + if self.hvac_mode == HVAC_MODE_HEAT: + return self._econet.heat_set_point + return None + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + return self._econet.heat_set_point + return None + + @property + def target_temperature_high(self): + """Return the higher bound temperature we try to reach.""" + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + return self._econet.cool_set_point + return None + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp: + self._econet.set_set_point(target_temp, None, None) + if target_temp_low or target_temp_high: + self._econet.set_set_point(None, target_temp_high, target_temp_low) + + @property + def is_aux_heat(self): + """Return true if aux heater.""" + return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT + + @property + def hvac_modes(self): + """Return hvac operation ie. heat, cool mode. + + Needs to be one of HVAC_MODE_*. + """ + return self.op_list + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool, mode. + + Needs to be one of HVAC_MODE_*. + """ + econet_mode = self._econet.mode + _current_op = HVAC_MODE_OFF + if econet_mode is not None: + _current_op = ECONET_STATE_TO_HA[econet_mode] + + return _current_op + + def set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + hvac_mode_to_set = HA_STATE_TO_ECONET.get(hvac_mode) + if hvac_mode_to_set is None: + raise ValueError(f"{hvac_mode} is not a valid mode.") + self._econet.set_mode(hvac_mode_to_set) + + def set_humidity(self, humidity: int): + """Set new target humidity.""" + self._econet.set_dehumidifier_set_point(humidity) + + @property + def fan_mode(self): + """Return the current fan mode.""" + econet_fan_mode = self._econet.fan_mode + + # Remove this after we figure out how to handle med lo and med hi + if econet_fan_mode in [ThermostatFanMode.MEDHI, ThermostatFanMode.MEDLO]: + econet_fan_mode = ThermostatFanMode.MEDIUM + + _current_fan_mode = FAN_AUTO + if econet_fan_mode is not None: + _current_fan_mode = ECONET_FAN_STATE_TO_HA[econet_fan_mode] + return _current_fan_mode + + @property + def fan_modes(self): + """Return the fan modes.""" + econet_fan_modes = self._econet.fan_modes + fan_list = [] + for mode in econet_fan_modes: + # Remove the MEDLO MEDHI once we figure out how to handle it + if mode not in [ + ThermostatFanMode.UNKNOWN, + ThermostatFanMode.MEDLO, + ThermostatFanMode.MEDHI, + ]: + fan_list.append(ECONET_FAN_STATE_TO_HA[mode]) + return fan_list + + def set_fan_mode(self, fan_mode): + """Set the fan mode.""" + self._econet.set_fan_mode(HA_FAN_STATE_TO_ECONET[fan_mode]) + + def turn_aux_heat_on(self): + """Turn auxiliary heater on.""" + self._econet.set_mode(ThermostatOperationMode.EMERGENCY_HEAT) + + def turn_aux_heat_off(self): + """Turn auxiliary heater off.""" + self._econet.set_mode(ThermostatOperationMode.HEATING) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._econet.set_point_limits[0] + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._econet.set_point_limits[1] + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return self._econet.dehumidifier_set_point_limits[0] + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return self._econet.dehumidifier_set_point_limits[1] diff --git a/homeassistant/components/econet/config_flow.py b/homeassistant/components/econet/config_flow.py index 78aff2eac8f..739606088f6 100644 --- a/homeassistant/components/econet/config_flow.py +++ b/homeassistant/components/econet/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN class EcoNetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 7e4cf0106ba..c658542295e 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -4,6 +4,6 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.12"], + "requirements": ["pyeconet==0.1.13"], "codeowners": ["@vangorra", "@w1ll1am23"] } \ No newline at end of file diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index e0ef7dc6ce9..0dfe8df7fb3 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -1,10 +1,11 @@ """Support for Rheem EcoNet water heaters.""" from pyeconet.equipment import EquipmentType +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( + DEVICE_CLASS_SIGNAL_STRENGTH, ENERGY_KILO_WATT_HOUR, PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, VOLUME_GALLONS, ) @@ -12,6 +13,7 @@ from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu" + TANK_HEALTH = "tank_health" AVAILIBLE_HOT_WATER = "availible_hot_water" COMPRESSOR_HEALTH = "compressor_health" @@ -22,32 +24,55 @@ ALERT_COUNT = "alert_count" WIFI_SIGNAL = "wifi_signal" RUNNING_STATE = "running_state" +SENSOR_NAMES_TO_ATTRIBUTES = { + TANK_HEALTH: "tank_health", + AVAILIBLE_HOT_WATER: "tank_hot_water_availability", + COMPRESSOR_HEALTH: "compressor_health", + OVERRIDE_STATUS: "override_status", + WATER_USAGE_TODAY: "todays_water_usage", + POWER_USAGE_TODAY: "todays_energy_usage", + ALERT_COUNT: "alert_count", + WIFI_SIGNAL: "wifi_signal", + RUNNING_STATE: "running_state", +} + +SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = { + TANK_HEALTH: PERCENTAGE, + AVAILIBLE_HOT_WATER: PERCENTAGE, + COMPRESSOR_HEALTH: PERCENTAGE, + OVERRIDE_STATUS: None, + WATER_USAGE_TODAY: VOLUME_GALLONS, + POWER_USAGE_TODAY: None, # Depends on unit type + ALERT_COUNT: None, + WIFI_SIGNAL: DEVICE_CLASS_SIGNAL_STRENGTH, + RUNNING_STATE: None, # This is just a string +} + async def async_setup_entry(hass, entry, async_add_entities): """Set up EcoNet sensor based on a config entry.""" + equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] sensors = [] + all_equipment = equipment[EquipmentType.WATER_HEATER].copy() + all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy()) + + for _equip in all_equipment: + for name, attribute in SENSOR_NAMES_TO_ATTRIBUTES.items(): + if getattr(_equip, attribute, None) is not None: + sensors.append(EcoNetSensor(_equip, name)) + # This is None to start with and all device have it + sensors.append(EcoNetSensor(_equip, WIFI_SIGNAL)) + for water_heater in equipment[EquipmentType.WATER_HEATER]: - if water_heater.tank_hot_water_availability is not None: - sensors.append(EcoNetSensor(water_heater, AVAILIBLE_HOT_WATER)) - if water_heater.tank_health is not None: - sensors.append(EcoNetSensor(water_heater, TANK_HEALTH)) - if water_heater.compressor_health is not None: - sensors.append(EcoNetSensor(water_heater, COMPRESSOR_HEALTH)) - if water_heater.override_status: - sensors.append(EcoNetSensor(water_heater, OVERRIDE_STATUS)) - if water_heater.running_state is not None: - sensors.append(EcoNetSensor(water_heater, RUNNING_STATE)) - # All units have this - sensors.append(EcoNetSensor(water_heater, ALERT_COUNT)) # These aren't part of the device and start off as None in pyeconet so always add them sensors.append(EcoNetSensor(water_heater, WATER_USAGE_TODAY)) sensors.append(EcoNetSensor(water_heater, POWER_USAGE_TODAY)) - sensors.append(EcoNetSensor(water_heater, WIFI_SIGNAL)) + async_add_entities(sensors) -class EcoNetSensor(EcoNetEntity): +class EcoNetSensor(EcoNetEntity, SensorEntity): """Define a Econet sensor.""" def __init__(self, econet_device, device_name): @@ -59,50 +84,21 @@ class EcoNetSensor(EcoNetEntity): @property def state(self): """Return sensors state.""" - if self._device_name == AVAILIBLE_HOT_WATER: - return self._econet.tank_hot_water_availability - if self._device_name == TANK_HEALTH: - return self._econet.tank_health - if self._device_name == COMPRESSOR_HEALTH: - return self._econet.compressor_health - if self._device_name == OVERRIDE_STATUS: - return self._econet.oveerride_status - if self._device_name == WATER_USAGE_TODAY: - if self._econet.todays_water_usage: - return round(self._econet.todays_water_usage, 2) - return None - if self._device_name == POWER_USAGE_TODAY: - if self._econet.todays_energy_usage: - return round(self._econet.todays_energy_usage, 2) - return None - if self._device_name == WIFI_SIGNAL: - if self._econet.wifi_signal: - return self._econet.wifi_signal - return None - if self._device_name == ALERT_COUNT: - return self._econet.alert_count - if self._device_name == RUNNING_STATE: - return self._econet.running_state - return None + value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name]) + if isinstance(value, float): + value = round(value, 2) + return value @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self._device_name == AVAILIBLE_HOT_WATER: - return PERCENTAGE - if self._device_name == TANK_HEALTH: - return PERCENTAGE - if self._device_name == COMPRESSOR_HEALTH: - return PERCENTAGE - if self._device_name == WATER_USAGE_TODAY: - return VOLUME_GALLONS + unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name] if self._device_name == POWER_USAGE_TODAY: if self._econet.energy_type == ENERGY_KILO_BRITISH_THERMAL_UNIT.upper(): - return ENERGY_KILO_BRITISH_THERMAL_UNIT - return ENERGY_KILO_WATT_HOUR - if self._device_name == WIFI_SIGNAL: - return SIGNAL_STRENGTH_DECIBELS_MILLIWATT - return None + unit_of_measurement = ENERGY_KILO_BRITISH_THERMAL_UNIT + else: + unit_of_measurement = ENERGY_KILO_WATT_HOUR + return unit_of_measurement @property def name(self): diff --git a/homeassistant/components/econet/translations/bg.json b/homeassistant/components/econet/translations/bg.json new file mode 100644 index 00000000000..cef3726d759 --- /dev/null +++ b/homeassistant/components/econet/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/hu.json b/homeassistant/components/econet/translations/hu.json new file mode 100644 index 00000000000..065c648d4a0 --- /dev/null +++ b/homeassistant/components/econet/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/id.json b/homeassistant/components/econet/translations/id.json new file mode 100644 index 00000000000..467b58a8d27 --- /dev/null +++ b/homeassistant/components/econet/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + }, + "title": "Siapkan Akun Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/ko.json b/homeassistant/components/econet/translations/ko.json index f5c1381b8b1..40735cdb6d0 100644 --- a/homeassistant/components/econet/translations/ko.json +++ b/homeassistant/components/econet/translations/ko.json @@ -14,7 +14,8 @@ "data": { "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" - } + }, + "title": "Rheem EcoNet \uacc4\uc815 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index af3399b53af..ed31e78af7c 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -18,7 +18,6 @@ from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, WaterHeaterEntity, ) -from homeassistant.const import TEMP_FAHRENHEIT from homeassistant.core import callback from . import EcoNetEntity @@ -77,11 +76,6 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """Return true if away mode is on.""" return self._econet.away - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - @property def current_operation(self): """Return current operation.""" @@ -160,6 +154,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """Get the latest energy usage.""" await self.water_heater.get_energy_usage() await self.water_heater.get_water_usage() + self.async_write_ha_state() self._poll = False def turn_away_mode_on(self): diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 6ad51e6c474..934833c0f95 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -189,7 +189,7 @@ class EcovacsVacuum(VacuumEntity): self.device.run(sucks.VacBotCommand(command, params)) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device-specific state attributes of this vacuum.""" data = {} data[ATTR_ERROR] = self._error diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 1d6ff61bf59..28711821f50 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -10,7 +10,7 @@ import logging from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, EVENT_HOMEASSISTANT_START, @@ -19,7 +19,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -97,7 +96,7 @@ def get_from_conf(config, config_key, length): return string -class EddystoneTemp(Entity): +class EddystoneTemp(SensorEntity): """Representation of a temperature sensor.""" def __init__(self, name, namespace, instance): @@ -171,15 +170,18 @@ class Monitor: ) for dev in self.devices: - if dev.namespace == namespace and dev.instance == instance: - if dev.temperature != temperature: - dev.temperature = temperature - dev.schedule_update_ha_state() + if ( + dev.namespace == namespace + and dev.instance == instance + and dev.temperature != temperature + ): + dev.temperature = temperature + dev.schedule_update_ha_state() def stop(self): """Signal runner to stop and join thread.""" if self.scanning: - _LOGGER.debug("Stopping...") + _LOGGER.debug("Stopping") self.scanner.stop() _LOGGER.debug("Stopped") self.scanning = False diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index dc0f51abe61..090b2780ec4 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -7,7 +7,7 @@ from sml import SmlGetListResponse from sml.asyncio import SmlProtocol import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -15,7 +15,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import Optional from homeassistant.util.dt import utcnow @@ -193,7 +192,7 @@ class EDL21: self._async_add_entities(new_entities, update_before_add=True) -class EDL21Entity(Entity): +class EDL21Entity(SensorEntity): """Entity reading values from EDL21 telegram.""" def __init__(self, electricity_id, obis, name, telegram): @@ -269,7 +268,7 @@ class EDL21Entity(Entity): return self._telegram.get("value") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Enumerate supported attributes.""" return { self._state_attrs[k]: v diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 02bc8fa0ccb..6e2ac1c01c7 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_CURRENCY, CONF_MONITORED_VARIABLES, @@ -13,7 +13,6 @@ from homeassistant.const import ( POWER_WATT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://engage.efergy.com/mobile_proxy/" @@ -94,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class EfergySensor(Entity): +class EfergySensor(SensorEntity): """Implementation of an Efergy sensor.""" def __init__(self, sensor_type, app_token, utc_offset, period, currency, sid=None): diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 43bcb4c2f09..ae0854ec244 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -1,6 +1,7 @@ """Support for Eight Sleep sensors.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from . import ( @@ -66,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(all_sensors, True) -class EightHeatSensor(EightSleepHeatEntity): +class EightHeatSensor(EightSleepHeatEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" def __init__(self, name, eight, sensor): @@ -110,7 +111,7 @@ class EightHeatSensor(EightSleepHeatEntity): self._state = self._usrobj.heating_level @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" return { ATTR_TARGET_HEAT: self._usrobj.target_heating_level, @@ -119,7 +120,7 @@ class EightHeatSensor(EightSleepHeatEntity): } -class EightUserSensor(EightSleepUserEntity): +class EightUserSensor(EightSleepUserEntity, SensorEntity): """Representation of an eight sleep user-based sensor.""" def __init__(self, name, eight, sensor, units): @@ -202,7 +203,7 @@ class EightUserSensor(EightSleepUserEntity): self._state = self._usrobj.current_values["stage"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" if self._attr is None: # Skip attributes if sensor type doesn't support @@ -289,7 +290,7 @@ class EightUserSensor(EightSleepUserEntity): return state_attr -class EightRoomSensor(EightSleepUserEntity): +class EightRoomSensor(EightSleepUserEntity, SensorEntity): """Representation of an eight sleep room sensor.""" def __init__(self, name, eight, sensor, units): diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index d6c849a4c5a..22d60406780 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -1,4 +1,6 @@ """Support for Elgato Key Lights.""" +import logging + from elgato import Elgato, ElgatoConnectionError from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -7,16 +9,10 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import DATA_ELGATO_CLIENT, DOMAIN -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Elgato Key Light components.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elgato Key Light from a config entry.""" session = async_get_clientsession(hass) @@ -30,6 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await elgato.info() except ElgatoConnectionError as exception: + logging.getLogger(__name__).debug("Unable to connect: %s", exception) raise ConfigEntryNotReady from exception hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index e9138afd86c..afdbe7e1cdc 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the Elgato Key Light integration.""" from __future__ import annotations -from typing import Any, Dict +from typing import Any from elgato import Elgato, ElgatoError import voluptuous as vol @@ -11,7 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import +from .const import CONF_SERIAL_NUMBER, DOMAIN class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): @@ -25,8 +25,8 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): serial_number: str async def async_step_user( - self, user_input: Dict[str, Any] | None = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if user_input is None: return self._async_show_setup_form() @@ -42,8 +42,8 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() async def async_step_zeroconf( - self, discovery_info: Dict[str, Any] - ) -> Dict[str, Any]: + self, discovery_info: dict[str, Any] + ) -> dict[str, Any]: """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] self.port = discovery_info[CONF_PORT] @@ -53,21 +53,22 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): except ElgatoError: return self.async_abort(reason="cannot_connect") + self._set_confirm_only() return self.async_show_form( step_id="zeroconf_confirm", description_placeholders={"serial_number": self.serial_number}, ) async def async_step_zeroconf_confirm( - self, _: Dict[str, Any] | None = None - ) -> Dict[str, Any]: + self, _: dict[str, Any] | None = None + ) -> dict[str, Any]: """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]: + self, errors: dict[str, str] | None = None + ) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -81,7 +82,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry(self) -> Dict[str, Any]: + def _async_create_entry(self) -> dict[str, Any]: 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 0648a4817bc..ae3d8274281 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Callable, Dict, List +from typing import Any, Callable from elgato import Elgato, ElgatoError, Info, State @@ -38,7 +38,7 @@ SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Elgato Key Light based on a config entry.""" elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] @@ -55,25 +55,20 @@ class ElgatoLight(LightEntity): info: Info, ): """Initialize Elgato Key Light.""" - self._brightness: int | None = None self._info: Info = info - self._state: bool | None = None - self._temperature: int | None = None - self._available = True + self._state: State | None = None self.elgato = elgato @property def name(self) -> str: """Return the name of the entity.""" # Return the product name, if display name is not set - if not self._info.display_name: - return self._info.product_name - return self._info.display_name + return self._info.display_name or self._info.product_name @property def available(self) -> bool: """Return True if entity is available.""" - return self._available + return self._state is not None @property def unique_id(self) -> str: @@ -83,12 +78,14 @@ class ElgatoLight(LightEntity): @property def brightness(self) -> int | None: """Return the brightness of this light between 1..255.""" - return self._brightness + assert self._state is not None + return round((self._state.brightness * 255) / 100) @property def color_temp(self) -> int | None: """Return the CT color value in mireds.""" - return self._temperature + assert self._state is not None + return self._state.temperature @property def min_mireds(self) -> int: @@ -108,7 +105,8 @@ class ElgatoLight(LightEntity): @property def is_on(self) -> bool: """Return the state of the light.""" - return bool(self._state) + assert self._state is not None + return self._state.on async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" @@ -116,7 +114,7 @@ class ElgatoLight(LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data: Dict[str, bool | int] = {ATTR_ON: True} + data: dict[str, bool | int] = {ATTR_ON: True} if ATTR_ON in kwargs: data[ATTR_ON] = kwargs[ATTR_ON] @@ -131,25 +129,22 @@ class ElgatoLight(LightEntity): await self.elgato.light(**data) except ElgatoError: _LOGGER.error("An error occurred while updating the Elgato Key Light") - self._available = False + self._state = None async def async_update(self) -> None: """Update Elgato entity.""" + restoring = self._state is None try: - state: State = await self.elgato.state() - except ElgatoError: - if self._available: - _LOGGER.error("An error occurred while updating the Elgato Key Light") - self._available = False - return - - self._available = True - self._brightness = round((state.brightness * 255) / 100) - self._state = state.on - self._temperature = state.temperature + self._state: State = await self.elgato.state() + if restoring: + _LOGGER.info("Connection restored") + except ElgatoError as err: + meth = _LOGGER.error if self._state else _LOGGER.debug + meth("An error occurred while updating the Elgato Key Light: %s", err) + self._state = None @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this Elgato Key Light.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 1da98a41211..9a166b86b8e 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,7 +3,7 @@ "name": "Elgato Key Light", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/elgato", - "requirements": ["elgato==1.0.0"], + "requirements": ["elgato==2.0.1"], "zeroconf": ["_elg._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum" diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json index 3c69fd4562a..ef6404bd92d 100644 --- a/homeassistant/components/elgato/translations/hu.json +++ b/homeassistant/components/elgato/translations/hu.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "Elgato Key Light: {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/id.json b/homeassistant/components/elgato/translations/id.json new file mode 100644 index 00000000000..b06691b9453 --- /dev/null +++ b/homeassistant/components/elgato/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Siapkan Elgato Key Light Anda untuk diintegrasikan dengan Home Assistant." + }, + "zeroconf_confirm": { + "description": "Ingin menambahkan Elgato Key Light dengan nomor seri `{serial_number}` ke Home Assistant?", + "title": "Perangkat Elgato Key Light yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/ko.json b/homeassistant/components/elgato/translations/ko.json index f2deb818431..2d3c7111b2e 100644 --- a/homeassistant/components/elgato/translations/ko.json +++ b/homeassistant/components/elgato/translations/ko.json @@ -14,10 +14,10 @@ "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" }, - "description": "Home Assistant \uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4." + "description": "Home Assistant\uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4." }, "zeroconf_confirm": { - "description": "Elgato Key Light \uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}`\uc758 Elgato Key Light\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ubc1c\uacac\ub41c Elgato Key Light \uae30\uae30" } } diff --git a/homeassistant/components/elgato/translations/nl.json b/homeassistant/components/elgato/translations/nl.json index 81035cc898f..fcda6a7ca84 100644 --- a/homeassistant/components/elgato/translations/nl.json +++ b/homeassistant/components/elgato/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dit Elgato Key Light apparaat is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken" }, "error": { @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "host": "Hostnaam of IP-adres", - "port": "Poortnummer" + "host": "Host", + "port": "Poort" }, "description": "Stel uw Elgato Key Light in om te integreren met Home Assistant." }, diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index b3d56e42325..a4d812850f7 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -6,11 +6,10 @@ import logging import eliqonline import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([EliqSensor(api, channel_id, name)], True) -class EliqSensor(Entity): +class EliqSensor(SensorEntity): """Implementation of an ELIQ Online sensor.""" def __init__(self, api, channel_id, name): diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index d50c5d65d90..568b3109227 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -52,7 +52,7 @@ SYNC_TIMEOUT = 120 _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = [ +PLATFORMS = [ "alarm_control_panel", "climate", "light", @@ -262,9 +262,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "keypads": {}, } - for component in SUPPORTED_DOMAINS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -289,8 +289,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SUPPORTED_DOMAINS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -429,7 +429,7 @@ class ElkEntity(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the default attributes of the element.""" return {**self._element.as_dict(), **self.initial_attrs()} diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 8f752cd9adf..756166c86a6 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -173,7 +173,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Attributes of the area.""" attrs = self.initial_attrs() elmt = self._element diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index b72cfa19335..86117824767 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -21,8 +21,7 @@ from homeassistant.const import ( from homeassistant.util import slugify from . import async_wait_for_elk_to_sync -from .const import CONF_AUTO_CONFIGURE -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_AUTO_CONFIGURE, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index c6442af2e44..e33196a08c0 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -8,6 +8,7 @@ from elkm1_lib.const import ( from elkm1_lib.util import pretty_const, username import voluptuous as vol +from homeassistant.components.sensor import SensorEntity from homeassistant.const import VOLT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -67,7 +68,7 @@ def temperature_to_state(temperature, undefined_temperature): return temperature if temperature > undefined_temperature else None -class ElkSensor(ElkAttachedEntity): +class ElkSensor(ElkAttachedEntity, SensorEntity): """Base representation of Elk-M1 sensor.""" def __init__(self, element, elk, elk_data): @@ -136,7 +137,7 @@ class ElkKeypad(ElkSensor): return "mdi:thermometer-lines" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Attributes of the sensor.""" attrs = self.initial_attrs() attrs["area"] = self._element.area + 1 @@ -163,7 +164,7 @@ class ElkPanel(ElkSensor): return "mdi:home" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Attributes of the sensor.""" attrs = self.initial_attrs() attrs["system_trouble_status"] = self._element.system_trouble_status @@ -190,7 +191,7 @@ class ElkSetting(ElkSensor): self._state = self._element.value @property - def device_state_attributes(self): + def extra_state_attributes(self): """Attributes of the sensor.""" attrs = self.initial_attrs() attrs["value_format"] = SettingFormat(self._element.value_format).name.lower() @@ -227,7 +228,7 @@ class ElkZone(ElkSensor): return f"mdi:{zone_icons.get(self._element.definition, 'alarm-bell')}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Attributes of the sensor.""" attrs = self.initial_attrs() attrs["physical_status"] = ZonePhysicalStatus( diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/elkm1/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/hu.json b/homeassistant/components/elkm1/translations/hu.json index dee4ed9ee0f..83862dfb75f 100644 --- a/homeassistant/components/elkm1/translations/hu.json +++ b/homeassistant/components/elkm1/translations/hu.json @@ -1,9 +1,15 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "user": { "data": { "password": "Jelsz\u00f3", + "protocol": "Protokoll", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } diff --git a/homeassistant/components/elkm1/translations/id.json b/homeassistant/components/elkm1/translations/id.json new file mode 100644 index 00000000000..e7ddd3cf9ee --- /dev/null +++ b/homeassistant/components/elkm1/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "address_already_configured": "ElkM1 dengan alamat ini sudah dikonfigurasi", + "already_configured": "ElkM1 dengan prefiks ini sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "address": "Alamat IP atau domain atau port serial jika terhubung melalui serial.", + "password": "Kata Sandi", + "prefix": "Prefiks unik (kosongkan jika hanya ada satu ElkM1).", + "protocol": "Protokol", + "temperature_unit": "Unit suhu yang digunakan ElkM1.", + "username": "Nama Pengguna" + }, + "description": "String alamat harus dalam format 'alamat[:port]' untuk 'aman' dan 'tidak aman'. Misalnya, '192.168.1.1'. Port bersifat opsional dan nilai baku adalah 2101 untuk 'tidak aman' dan 2601 untuk 'aman'. Untuk protokol serial, alamat harus dalam format 'tty[:baud]'. Misalnya, '/dev/ttyS1'. Baud bersifat opsional dan nilai bakunya adalah 115200.", + "title": "Hubungkan ke Kontrol Elk-M1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/ko.json b/homeassistant/components/elkm1/translations/ko.json index fb8c22ba5b2..507f741676a 100644 --- a/homeassistant/components/elkm1/translations/ko.json +++ b/homeassistant/components/elkm1/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "address_already_configured": "\uc774 \uc8fc\uc18c\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_configured": "\uc774 \uc811\ub450\uc0ac\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "address_already_configured": "\uc774 \uc8fc\uc18c\ub85c ElkM1\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uc774 \uc811\ub450\uc0ac\ub85c ElkM1\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", @@ -19,7 +19,7 @@ "temperature_unit": "ElkM1 \uc774 \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704.", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548'\uc5d0 \ub300\ud574 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \ud1b5\uc2e0\uc18d\ub3c4 \ubc14\uc6b0\ub4dc\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.", + "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548'\uc5d0 \ub300\ud574 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \uc804\uc1a1 \uc18d\ub3c4\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.", "title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json index 9e7adf71c4b..de51e67b206 100644 --- a/homeassistant/components/elkm1/translations/nl.json +++ b/homeassistant/components/elkm1/translations/nl.json @@ -5,7 +5,7 @@ "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -13,11 +13,11 @@ "user": { "data": { "address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.", - "password": "Wachtwoord (alleen beveiligd).", + "password": "Wachtwoord", "prefix": "Een uniek voorvoegsel (laat dit leeg als u maar \u00e9\u00e9n ElkM1 heeft).", "protocol": "Protocol", "temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.", - "username": "Gebruikersnaam (alleen beveiligd)." + "username": "Gebruikersnaam" }, "description": "De adresreeks moet de vorm 'adres [: poort]' hebben voor 'veilig' en 'niet-beveiligd'. Voorbeeld: '192.168.1.1'. De poort is optioneel en is standaard 2101 voor 'niet beveiligd' en 2601 voor 'beveiligd'. Voor het seri\u00eble protocol moet het adres de vorm 'tty [: baud]' hebben. Voorbeeld: '/ dev / ttyS1'. De baud is optioneel en is standaard ingesteld op 115200.", "title": "Maak verbinding met Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json index 48a950f1cca..954722ecf52 100644 --- a/homeassistant/components/elkm1/translations/ru.json +++ b/homeassistant/components/elkm1/translations/ru.json @@ -17,7 +17,7 @@ "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1)", "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u043e\u0432 'secure' \u0438 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'non-secure' \u0438 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 'serial' \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u043d \u0440\u0430\u0432\u0435\u043d 115200.", "title": "Elk-M1 Control" diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 12b21c23d1a..48eb9675277 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -74,7 +74,7 @@ class SmartPlugSwitch(SwitchEntity): self._pca.turn_off(self._device_id) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._emeter_params diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 1cbc893f98b..5656a1f1486 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -96,12 +96,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= active_emby_devices[dev_id] = new new_devices.append(new) - elif dev_id in inactive_emby_devices: - if emby.devices[dev_id].state != "Off": - add = inactive_emby_devices.pop(dev_id) - active_emby_devices[dev_id] = add - _LOGGER.debug("Showing %s, item: %s", dev_id, add) - add.set_available(True) + elif ( + dev_id in inactive_emby_devices and emby.devices[dev_id].state != "Off" + ): + add = inactive_emby_devices.pop(dev_id) + active_emby_devices[dev_id] = add + _LOGGER.debug("Showing %s, item: %s", dev_id, add) + add.set_available(True) if new_devices: _LOGGER.debug("Adding new devices: %s", new_devices) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index dca9c870022..bfc86db387e 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_API_KEY, CONF_ID, @@ -19,7 +19,6 @@ from homeassistant.const import ( ) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -93,13 +92,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for elem in data.data: - if exclude_feeds is not None: - if int(elem["id"]) in exclude_feeds: - continue + if exclude_feeds is not None and int(elem["id"]) in exclude_feeds: + continue - if include_only_feeds is not None: - if int(elem["id"]) not in include_only_feeds: - continue + if include_only_feeds is not None and int(elem["id"]) not in include_only_feeds: + continue name = None if sensor_names is not None: @@ -125,7 +122,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class EmonCmsSensor(Entity): +class EmonCmsSensor(SensorEntity): """Implementation of an Emoncms sensor.""" def __init__( @@ -175,7 +172,7 @@ class EmonCmsSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the sensor.""" return { ATTR_FEEDID: self._elem["id"], diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 11ad80688a3..3864a2651f8 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,4 +1,5 @@ """Support for local control of entities by emulating a Philips Hue bridge.""" +from contextlib import suppress import logging from aiohttp import web @@ -341,8 +342,6 @@ class Config: def _load_json(filename): """Load JSON, handling invalid syntax.""" - try: + with suppress(HomeAssistantError): return load_json(filename) - except HomeAssistantError: - pass return {} diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 1630405a73e..f97636a46c0 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -391,11 +391,9 @@ class HueOneLightChangeView(HomeAssistantView): return self.json_message("Bad request", HTTP_BAD_REQUEST) if HUE_API_STATE_XY in request_json: try: - parsed[STATE_XY] = tuple( - ( - float(request_json[HUE_API_STATE_XY][0]), - float(request_json[HUE_API_STATE_XY][1]), - ) + parsed[STATE_XY] = ( + float(request_json[HUE_API_STATE_XY][0]), + float(request_json[HUE_API_STATE_XY][1]), ) except ValueError: _LOGGER.error("Unable to parse data (2): %s", request_json) @@ -441,11 +439,13 @@ class HueOneLightChangeView(HomeAssistantView): # saturation and color temp if entity.domain == light.DOMAIN: if parsed[STATE_ON]: - if entity_features & SUPPORT_BRIGHTNESS: - if parsed[STATE_BRIGHTNESS] is not None: - data[ATTR_BRIGHTNESS] = hue_brightness_to_hass( - parsed[STATE_BRIGHTNESS] - ) + if ( + entity_features & SUPPORT_BRIGHTNESS + and parsed[STATE_BRIGHTNESS] is not None + ): + data[ATTR_BRIGHTNESS] = hue_brightness_to_hass( + parsed[STATE_BRIGHTNESS] + ) if entity_features & SUPPORT_COLOR: if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): @@ -468,13 +468,17 @@ class HueOneLightChangeView(HomeAssistantView): if parsed[STATE_XY] is not None: data[ATTR_XY_COLOR] = parsed[STATE_XY] - if entity_features & SUPPORT_COLOR_TEMP: - if parsed[STATE_COLOR_TEMP] is not None: - data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] + if ( + entity_features & SUPPORT_COLOR_TEMP + and parsed[STATE_COLOR_TEMP] is not None + ): + data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] - if entity_features & SUPPORT_TRANSITION: - if parsed[STATE_TRANSITON] is not None: - data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10 + if ( + entity_features & SUPPORT_TRANSITION + and parsed[STATE_TRANSITON] is not None + ): + data[ATTR_TRANSITION] = parsed[STATE_TRANSITON] / 10 # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: @@ -491,11 +495,13 @@ class HueOneLightChangeView(HomeAssistantView): # only setting the temperature service = None - if entity_features & SUPPORT_TARGET_TEMPERATURE: - if parsed[STATE_BRIGHTNESS] is not None: - domain = entity.domain - service = SERVICE_SET_TEMPERATURE - data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS] + if ( + entity_features & SUPPORT_TARGET_TEMPERATURE + and parsed[STATE_BRIGHTNESS] is not None + ): + domain = entity.domain + service = SERVICE_SET_TEMPERATURE + data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS] # If the requested entity is a humidifier, set the humidity elif entity.domain == humidifier.DOMAIN: @@ -507,43 +513,48 @@ class HueOneLightChangeView(HomeAssistantView): # If the requested entity is a media player, convert to volume elif entity.domain == media_player.DOMAIN: - if entity_features & SUPPORT_VOLUME_SET: - if parsed[STATE_BRIGHTNESS] is not None: - turn_on_needed = True - domain = entity.domain - service = SERVICE_VOLUME_SET - # Convert 0-100 to 0.0-1.0 - data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0 + if ( + entity_features & SUPPORT_VOLUME_SET + and parsed[STATE_BRIGHTNESS] is not None + ): + turn_on_needed = True + domain = entity.domain + service = SERVICE_VOLUME_SET + # Convert 0-100 to 0.0-1.0 + data[ATTR_MEDIA_VOLUME_LEVEL] = parsed[STATE_BRIGHTNESS] / 100.0 # 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: - if parsed[STATE_BRIGHTNESS] is not None: - domain = entity.domain - service = SERVICE_SET_COVER_POSITION - data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS] + if ( + entity_features & SUPPORT_SET_POSITION + and parsed[STATE_BRIGHTNESS] is not None + ): + domain = entity.domain + service = SERVICE_SET_COVER_POSITION + data[ATTR_POSITION] = parsed[STATE_BRIGHTNESS] # If the requested entity is a fan, convert to speed - elif entity.domain == fan.DOMAIN: - if entity_features & SUPPORT_SET_SPEED: - if parsed[STATE_BRIGHTNESS] is not None: - domain = entity.domain - # Convert 0-100 to a fan speed - brightness = parsed[STATE_BRIGHTNESS] - if brightness == 0: - data[ATTR_SPEED] = SPEED_OFF - elif 0 < brightness <= 33.3: - data[ATTR_SPEED] = SPEED_LOW - elif 33.3 < brightness <= 66.6: - data[ATTR_SPEED] = SPEED_MEDIUM - elif 66.6 < brightness <= 100: - data[ATTR_SPEED] = SPEED_HIGH + elif ( + entity.domain == fan.DOMAIN + and entity_features & SUPPORT_SET_SPEED + and parsed[STATE_BRIGHTNESS] is not None + ): + domain = entity.domain + # Convert 0-100 to a fan speed + brightness = parsed[STATE_BRIGHTNESS] + if brightness == 0: + data[ATTR_SPEED] = SPEED_OFF + elif 0 < brightness <= 33.3: + data[ATTR_SPEED] = SPEED_LOW + elif 33.3 < brightness <= 66.6: + data[ATTR_SPEED] = SPEED_MEDIUM + elif 66.6 < brightness <= 100: + data[ATTR_SPEED] = SPEED_HIGH # Map the off command to on if entity.domain in config.off_maps_to_on_domains: diff --git a/homeassistant/components/emulated_roku/translations/hu.json b/homeassistant/components/emulated_roku/translations/hu.json index 3d490ddbfeb..bccfe3bdcab 100644 --- a/homeassistant/components/emulated_roku/translations/hu.json +++ b/homeassistant/components/emulated_roku/translations/hu.json @@ -1,9 +1,12 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "step": { "user": { "data": { - "host_ip": "Hoszt IP", + "host_ip": "Hoszt IP c\u00edm", "listen_port": "Port figyel\u00e9se", "name": "N\u00e9v" }, @@ -11,5 +14,5 @@ } } }, - "title": "EmulatedRoku" + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/id.json b/homeassistant/components/emulated_roku/translations/id.json new file mode 100644 index 00000000000..9ffcedf5d19 --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Umumkan Alamat IP", + "advertise_port": "Umumkan Port", + "host_ip": "Alamat IP Host", + "name": "Nama", + "upnp_bind_multicast": "Bind multicast (True/False)" + }, + "title": "Tentukan konfigurasi server" + } + } + }, + "title": "Emulasi Roku" +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/nl.json b/homeassistant/components/emulated_roku/translations/nl.json index 54d544faee8..dd988985250 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": "Adverteer IP", - "advertise_port": "Adverterenpoort", + "advertise_ip": "IP-adres zichtbaar", + "advertise_port": "Adverteer Poort", "host_ip": "Host IP", "listen_port": "Luisterpoort", "name": "Naam", @@ -17,5 +17,5 @@ } } }, - "title": "EmulatedRoku" + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 4baa6aaf047..1c32b1ab805 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -251,7 +251,7 @@ class Enigma2Device(MediaPlayerEntity): self.e2_box.update() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes. isRecording: Is the box currently recording. diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py index 7fce66d54e5..3f4309e3507 100644 --- a/homeassistant/components/enocean/config_flow.py +++ b/homeassistant/components/enocean/config_flow.py @@ -7,8 +7,7 @@ from homeassistant.config_entries import CONN_CLASS_ASSUMED from homeassistant.const import CONF_DEVICE from . import dongle -from .const import DOMAIN # pylint:disable=unused-import -from .const import ERROR_INVALID_DONGLE_PATH, LOGGER +from .const import DOMAIN, ERROR_INVALID_DONGLE_PATH, LOGGER class EnOceanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 011dcdafdb6..1814efb9c87 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -1,7 +1,7 @@ """Support for EnOcean sensors.""" import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ID, @@ -101,7 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanWindowHandle(dev_id, dev_name)]) -class EnOceanSensor(EnOceanEntity, RestoreEntity): +class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): """Representation of an EnOcean sensor device such as a power meter.""" def __init__(self, dev_id, dev_name, sensor_type): diff --git a/homeassistant/components/enocean/translations/hu.json b/homeassistant/components/enocean/translations/hu.json new file mode 100644 index 00000000000..065747fb39d --- /dev/null +++ b/homeassistant/components/enocean/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/id.json b/homeassistant/components/enocean/translations/id.json new file mode 100644 index 00000000000..ccadfe55982 --- /dev/null +++ b/homeassistant/components/enocean/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Jalur dongle tidak valid", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_dongle_path": "Tidak ada dongle valid yang ditemukan untuk jalur ini" + }, + "step": { + "detect": { + "data": { + "path": "Jalur dongle USB" + }, + "title": "Pilih jalur ke dongle ENOcean Anda" + }, + "manual": { + "data": { + "path": "Jalur dongle USB" + }, + "title": "Masukkan jalur ke dongle ENOcean Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/ko.json b/homeassistant/components/enocean/translations/ko.json index ba109ed58c0..a7480a72b3b 100644 --- a/homeassistant/components/enocean/translations/ko.json +++ b/homeassistant/components/enocean/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "\ub3d9\uae00 \uacbd\ub85c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "invalid_dongle_path": "\uc774 \uacbd\ub85c\uc5d0 \uc720\ud6a8\ud55c \ub3d9\uae00\uc774 \uc5c6\uc2b5\ub2c8\ub2e4" @@ -18,7 +18,7 @@ "data": { "path": "USB \ub3d9\uae00 \uacbd\ub85c" }, - "title": "ENOcean \ub3d9\uae00 \uacbd\ub85c \uc785\ub825\ud558\uae30" + "title": "ENOcean \ub3d9\uae00 \uacbd\ub85c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } } } diff --git a/homeassistant/components/enocean/translations/nl.json b/homeassistant/components/enocean/translations/nl.json index 79aaec23123..c7dd4985133 100644 --- a/homeassistant/components/enocean/translations/nl.json +++ b/homeassistant/components/enocean/translations/nl.json @@ -1,7 +1,25 @@ { "config": { "abort": { + "invalid_dongle_path": "Ongeldig dongle-pad", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "invalid_dongle_path": "Geen geldige dongle gevonden voor dit pad" + }, + "step": { + "detect": { + "data": { + "path": "USB dongle pad" + }, + "title": "Selecteer het pad naar uw ENOcean-dongle" + }, + "manual": { + "data": { + "path": "USB-dongle-pad" + }, + "title": "Voer het pad naar uw ENOcean dongle in" + } } } } \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 64b4fdf66ad..dd1b10c870b 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -8,7 +8,7 @@ from envoy_reader.envoy_reader import EnvoyReader import httpx import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, @@ -156,7 +156,7 @@ async def async_setup_platform( async_add_entities(entities) -class Envoy(CoordinatorEntity): +class Envoy(CoordinatorEntity, SensorEntity): """Envoy entity.""" def __init__(self, sensor_type, name, serial_number, unit, coordinator): @@ -202,7 +202,7 @@ class Envoy(CoordinatorEntity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if ( self._type == "inverters" diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 883b5c43d7e..c9c530c6b08 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from enturclient import EnturPublicTransportData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, @@ -15,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -148,7 +147,7 @@ class EnturProxy: return self._api.get_stop_info(stop_id) -class EnturPublicTransportSensor(Entity): +class EnturPublicTransportSensor(SensorEntity): """Implementation of a Entur public transport sensor.""" def __init__(self, api: EnturProxy, name: str, stop: str, show_on_map: bool): @@ -172,7 +171,7 @@ class EnturPublicTransportSensor(Entity): return self._state @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION self._attributes[ATTR_STOP_ID] = self._stop diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 66079ac73ff..019dcb1aee5 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -1,7 +1,7 @@ """Support for the Environment Canada radar imagery.""" import datetime -from env_canada import ECRadar # pylint: disable=import-error +from env_canada import ECRadar import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -81,7 +81,7 @@ class ECCamera(Camera): return "Environment Canada Radar" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_UPDATED: self.timestamp} diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index a8772909f68..0f0fb04fd00 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -3,10 +3,10 @@ from datetime import datetime, timedelta import logging import re -from env_canada import ECData # pylint: disable=import-error +from env_canada import ECData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LOCATION, @@ -15,7 +15,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) -class ECSensor(Entity): +class ECSensor(SensorEntity): """Implementation of an Environment Canada sensor.""" def __init__(self, sensor_type, ec_data): @@ -95,7 +94,7 @@ class ECSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._attr diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index dd2252a585f..9abbc33bc93 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -2,7 +2,7 @@ import datetime import re -from env_canada import ECData # pylint: disable=import-error +from env_canada import ECData import voluptuous as vol from homeassistant.components.weather import ( diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 873d9935ee8..137d6aee853 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, @@ -14,7 +14,6 @@ from homeassistant.const import ( VOLT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class EnvirophatSensor(Entity): +class EnvirophatSensor(SensorEntity): """Representation of an Enviro pHAT sensor.""" def __init__(self, data, sensor_types): diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 73e20eea92c..75d4bff3dd1 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -144,9 +144,7 @@ async def async_setup(hass, config): @callback def connection_fail_callback(data): """Network failure callback.""" - _LOGGER.error( - "Could not establish a connection with the Envisalink- retrying..." - ) + _LOGGER.error("Could not establish a connection with the Envisalink- retrying") if not sync_connect.done(): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) sync_connect.set_result(True) @@ -162,13 +160,13 @@ async def async_setup(hass, config): @callback def zones_updated_callback(data): """Handle zone timer updates.""" - _LOGGER.debug("Envisalink sent a zone update event. Updating zones...") + _LOGGER.debug("Envisalink sent a zone update event. Updating zones") async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data) @callback def alarm_data_updated_callback(data): """Handle non-alarm based info updates.""" - _LOGGER.debug("Envisalink sent new alarm info. Updating alarms...") + _LOGGER.debug("Envisalink sent new alarm info. Updating alarms") async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data) @callback diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index 54445660484..22089ee7907 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -56,7 +56,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attr = {} diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 3f3711b2e40..6fd7f32c6fe 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -1,9 +1,9 @@ """Support for Envisalink sensors (shows panel info).""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import ( CONF_PARTITIONNAME, @@ -37,7 +37,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) -class EnvisalinkSensor(EnvisalinkDevice, Entity): +class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): """Representation of an Envisalink keypad.""" def __init__(self, hass, partition_name, partition_number, info, controller): @@ -66,7 +66,7 @@ class EnvisalinkSensor(EnvisalinkDevice, Entity): return self._info["status"]["alpha"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._info["status"] diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index c7278ed2cc9..51d464dacb5 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -48,9 +48,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.warning("Cannot connect to projector %s", entry.data[CONF_HOST]) return False hass.data[DOMAIN][entry.entry_id] = projector - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -60,8 +60,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 24ed62067f9..6115cdd6ef4 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -215,7 +215,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): await self._projector.send_command(BACK) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" if self._cmode is None: return {} diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json index 5ff60755bfd..f2a380903ec 100644 --- a/homeassistant/components/epson/translations/hu.json +++ b/homeassistant/components/epson/translations/hu.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "host": "Gazdag\u00e9p", + "host": "Hoszt", "name": "N\u00e9v", "port": "Port" } diff --git a/homeassistant/components/epson/translations/id.json b/homeassistant/components/epson/translations/id.json new file mode 100644 index 00000000000..ba2d36424f9 --- /dev/null +++ b/homeassistant/components/epson/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 7c6042c8959..22f74e1c0b1 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -4,11 +4,10 @@ from datetime import timedelta from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity MONITORED_CONDITIONS = { "black": ["Ink level Black", PERCENTAGE, "mdi:water"], @@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors, True) -class EpsonPrinterCartridge(Entity): +class EpsonPrinterCartridge(SensorEntity): """Representation of a cartridge sensor.""" def __init__(self, api, cartridgeidx): diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 737b3fe357a..f803c9c0bd5 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -1,7 +1,7 @@ """Support for eQ-3 Bluetooth Smart thermostats.""" import logging -from bluepy.btle import BTLEException # pylint: disable=import-error, no-name-in-module +from bluepy.btle import BTLEException # pylint: disable=import-error import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -155,7 +155,7 @@ class EQ3BTSmartThermostat(ClimateEntity): return self._thermostat.max_temp @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" dev_specific = { ATTR_STATE_AWAY_END: self._thermostat.away_end, diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 6ce411b5169..0caf00af8ef 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,9 +1,11 @@ """Support for esphome devices.""" +from __future__ import annotations + import asyncio import functools import logging import math -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable from aioesphomeapi import ( APIClient, @@ -16,12 +18,14 @@ from aioesphomeapi import ( UserServiceArgType, ) import voluptuous as vol +from zeroconf import DNSPointer, DNSRecord, RecordUpdateListener, Zeroconf from homeassistant import const from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, + CONF_MODE, CONF_PASSWORD, CONF_PORT, EVENT_HOMEASSISTANT_STOP, @@ -35,9 +39,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event 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 ConfigType, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData @@ -51,11 +56,6 @@ STORAGE_VERSION = 1 CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Stub to allow setting up this component.""" - return True - - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the esphome component.""" hass.data.setdefault(DOMAIN, {}) @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await cli.send_home_assistant_state(entity_id, new_state.state) async def _send_home_assistant_state( - entity_id: str, new_state: Optional[State] + entity_id: str, new_state: State | None ) -> None: """Forward Home Assistant states to ESPHome.""" await cli.send_home_assistant_state(entity_id, new_state.state) @@ -195,7 +195,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool # Re-connection logic will trigger after this await cli.disconnect() - try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host, on_login) + reconnect_logic = ReconnectLogic( + hass, cli, entry, host, on_login, zeroconf_instance + ) async def complete_setup() -> None: """Complete the config entry setup.""" @@ -203,81 +205,250 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await entry_data.async_update_static_infos(hass, entry, infos) await _setup_services(hass, entry_data, services) - # Create connection attempt outside of HA's tracked task in order - # not to delay startup. - hass.loop.create_task(try_connect(is_disconnect=False)) + await reconnect_logic.start() + entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) hass.async_create_task(complete_setup()) return True -async def _setup_auto_reconnect_logic( - hass: HomeAssistantType, cli: APIClient, entry: ConfigEntry, host: str, on_login -): - """Set up the re-connect logic for the API client.""" +class ReconnectLogic(RecordUpdateListener): + """Reconnectiong logic handler for ESPHome config entries. - async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None: - """Try connecting to the API client. Will retry if not successful.""" - if entry.entry_id not in hass.data[DOMAIN]: + Contains two reconnect strategies: + - Connect with increasing time between connection attempts. + - Listen to zeroconf mDNS records, if any records are found for this device, try reconnecting immediately. + """ + + def __init__( + self, + hass: HomeAssistantType, + cli: APIClient, + entry: ConfigEntry, + host: str, + on_login, + zc: Zeroconf, + ): + """Initialize ReconnectingLogic.""" + self._hass = hass + self._cli = cli + self._entry = entry + self._host = host + self._on_login = on_login + self._zc = zc + # Flag to check if the device is connected + self._connected = True + self._connected_lock = asyncio.Lock() + self._zc_lock = asyncio.Lock() + self._zc_listening = False + # Event the different strategies use for issuing a reconnect attempt. + self._reconnect_event = asyncio.Event() + # The task containing the infinite reconnect loop while running + self._loop_task: asyncio.Task | None = None + # How many reconnect attempts have there been already, used for exponential wait time + self._tries = 0 + self._tries_lock = asyncio.Lock() + # Track the wait task to cancel it on HA shutdown + self._wait_task: asyncio.Task | None = None + self._wait_task_lock = asyncio.Lock() + + @property + def _entry_data(self) -> RuntimeEntryData | None: + return self._hass.data[DOMAIN].get(self._entry.entry_id) + + async def _on_disconnect(self): + """Log and issue callbacks when disconnecting.""" + if self._entry_data is None: + return + # This can happen often depending on WiFi signal strength. + # So therefore all these connection warnings are logged + # as infos. The "unavailable" logic will still trigger so the + # user knows if the device is not connected. + _LOGGER.info("Disconnected from ESPHome API for %s", self._host) + + # Run disconnect hooks + for disconnect_cb in self._entry_data.disconnect_callbacks: + disconnect_cb() + self._entry_data.disconnect_callbacks = [] + self._entry_data.available = False + self._entry_data.async_update_device_state(self._hass) + await self._start_zc_listen() + + # Reset tries + async with self._tries_lock: + self._tries = 0 + # Connected needs to be reset before the reconnect event (opposite order of check) + async with self._connected_lock: + self._connected = False + self._reconnect_event.set() + + async def _wait_and_start_reconnect(self): + """Wait for exponentially increasing time to issue next reconnect event.""" + async with self._tries_lock: + tries = self._tries + # If not first re-try, wait and print message + # Cap wait time at 1 minute. This is because while working on the + # device (e.g. soldering stuff), users don't want to have to wait + # a long time for their device to show up in HA again (this was + # mentioned a lot in early feedback) + tries = min(tries, 10) # prevent OverflowError + wait_time = int(round(min(1.8 ** tries, 60.0))) + if tries == 1: + _LOGGER.info("Trying to reconnect to %s in the background", self._host) + _LOGGER.debug("Retrying %s in %d seconds", self._host, wait_time) + await asyncio.sleep(wait_time) + async with self._wait_task_lock: + self._wait_task = None + self._reconnect_event.set() + + async def _try_connect(self): + """Try connecting to the API client.""" + async with self._tries_lock: + tries = self._tries + self._tries += 1 + + try: + await self._cli.connect(on_stop=self._on_disconnect, login=True) + except APIConnectionError as error: + level = logging.WARNING if tries == 0 else logging.DEBUG + _LOGGER.log( + level, + "Can't connect to ESPHome API for %s (%s): %s", + self._entry.unique_id, + self._host, + error, + ) + await self._start_zc_listen() + # Schedule re-connect in event loop in order not to delay HA + # startup. First connect is scheduled in tracked tasks. + async with self._wait_task_lock: + # Allow only one wait task at a time + # can happen if mDNS record received while waiting, then use existing wait task + if self._wait_task is not None: + return + + self._wait_task = self._hass.loop.create_task( + self._wait_and_start_reconnect() + ) + else: + _LOGGER.info("Successfully connected to %s", self._host) + async with self._tries_lock: + self._tries = 0 + async with self._connected_lock: + self._connected = True + await self._stop_zc_listen() + self._hass.async_create_task(self._on_login()) + + async def _reconnect_once(self): + # Wait and clear reconnection event + await self._reconnect_event.wait() + self._reconnect_event.clear() + + # If in connected state, do not try to connect again. + async with self._connected_lock: + if self._connected: + return False + + # Check if the entry got removed or disabled, in which case we shouldn't reconnect + if self._entry.entry_id not in self._hass.data[DOMAIN]: # When removing/disconnecting manually return - device_registry = await hass.helpers.device_registry.async_get_registry() - devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + device_registry = self._hass.helpers.device_registry.async_get(self._hass) + devices = dr.async_entries_for_config_entry( + device_registry, self._entry.entry_id + ) for device in devices: # There is only one device in ESPHome if device.disabled: # Don't attempt to connect if it's disabled return - data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] - for disconnect_cb in data.disconnect_callbacks: - disconnect_cb() - data.disconnect_callbacks = [] - data.available = False - data.async_update_device_state(hass) + await self._try_connect() - if is_disconnect: - # This can happen often depending on WiFi signal strength. - # So therefore all these connection warnings are logged - # as infos. The "unavailable" logic will still trigger so the - # user knows if the device is not connected. - _LOGGER.info("Disconnected from ESPHome API for %s", host) + async def _reconnect_loop(self): + while True: + try: + await self._reconnect_once() + except asyncio.CancelledError: # pylint: disable=try-except-raise + raise + except Exception: # pylint: disable=broad-except + _LOGGER.error("Caught exception while reconnecting", exc_info=True) - if tries != 0: - # If not first re-try, wait and print message - # Cap wait time at 1 minute. This is because while working on the - # device (e.g. soldering stuff), users don't want to have to wait - # a long time for their device to show up in HA again (this was - # mentioned a lot in early feedback) - # - # In the future another API will be set up so that the ESP can - # notify HA of connectivity directly, but for new we'll use a - # really short reconnect interval. - tries = min(tries, 10) # prevent OverflowError - wait_time = int(round(min(1.8 ** tries, 60.0))) - _LOGGER.info("Trying to reconnect to %s in %s seconds", host, wait_time) - await asyncio.sleep(wait_time) + async def start(self): + """Start the reconnecting logic background task.""" + # Create reconnection loop outside of HA's tracked tasks in order + # not to delay startup. + self._loop_task = self._hass.loop.create_task(self._reconnect_loop()) - try: - await cli.connect(on_stop=try_connect, login=True) - except APIConnectionError as error: - _LOGGER.info( - "Can't connect to ESPHome API for %s (%s): %s", - entry.unique_id, - host, - error, - ) - # Schedule re-connect in event loop in order not to delay HA - # startup. First connect is scheduled in tracked tasks. - data.reconnect_task = hass.loop.create_task( - try_connect(tries + 1, is_disconnect=False) - ) - else: - _LOGGER.info("Successfully connected to %s", host) - hass.async_create_task(on_login()) + async with self._connected_lock: + self._connected = False + self._reconnect_event.set() - return try_connect + async def stop(self): + """Stop the reconnecting logic background task. Does not disconnect the client.""" + if self._loop_task is not None: + self._loop_task.cancel() + self._loop_task = None + async with self._wait_task_lock: + if self._wait_task is not None: + self._wait_task.cancel() + self._wait_task = None + await self._stop_zc_listen() + + async def _start_zc_listen(self): + """Listen for mDNS records. + + This listener allows us to schedule a reconnect as soon as a + received mDNS record indicates the node is up again. + """ + async with self._zc_lock: + if not self._zc_listening: + await self._hass.async_add_executor_job( + self._zc.add_listener, self, None + ) + self._zc_listening = True + + async def _stop_zc_listen(self): + """Stop listening for zeroconf updates.""" + async with self._zc_lock: + if self._zc_listening: + await self._hass.async_add_executor_job(self._zc.remove_listener, self) + self._zc_listening = False + + @callback + def stop_callback(self): + """Stop as an async callback function.""" + self._hass.async_create_task(self.stop()) + + @callback + def _set_reconnect(self): + self._reconnect_event.set() + + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: + """Listen to zeroconf updated mDNS records.""" + if not isinstance(record, DNSPointer): + # We only consider PTR records and match using the alias name + return + if self._entry_data is None or self._entry_data.device_info is None: + # Either the entry was already teared down or we haven't received device info yet + return + filter_alias = f"{self._entry_data.device_info.name}._esphomelib._tcp.local." + if record.alias != filter_alias: + return + + # This is a mDNS record from the device and could mean it just woke up + # Check if already connected, no lock needed for this access + if self._connected: + return + + # Tell reconnection logic to retry connection attempt now (even before reconnect timer finishes) + _LOGGER.debug( + "%s: Triggering reconnect because of received mDNS record %s", + self._host, + record, + ) + self._hass.add_job(self._set_reconnect) async def _async_setup_device_registry( @@ -304,17 +475,63 @@ async def _register_service( ): service_name = f"{entry_data.device_info.name}_{service.name}" schema = {} + fields = {} + for arg in service.args: - schema[vol.Required(arg.name)] = { - UserServiceArgType.BOOL: cv.boolean, - UserServiceArgType.INT: vol.Coerce(int), - UserServiceArgType.FLOAT: vol.Coerce(float), - UserServiceArgType.STRING: cv.string, - UserServiceArgType.BOOL_ARRAY: [cv.boolean], - UserServiceArgType.INT_ARRAY: [vol.Coerce(int)], - UserServiceArgType.FLOAT_ARRAY: [vol.Coerce(float)], - UserServiceArgType.STRING_ARRAY: [cv.string], + metadata = { + UserServiceArgType.BOOL: { + "validator": cv.boolean, + "example": "False", + "selector": {"boolean": None}, + }, + UserServiceArgType.INT: { + "validator": vol.Coerce(int), + "example": "42", + "selector": {"number": {CONF_MODE: "box"}}, + }, + UserServiceArgType.FLOAT: { + "validator": vol.Coerce(float), + "example": "12.3", + "selector": {"number": {CONF_MODE: "box", "step": 1e-3}}, + }, + UserServiceArgType.STRING: { + "validator": cv.string, + "example": "Example text", + "selector": {"text": None}, + }, + UserServiceArgType.BOOL_ARRAY: { + "validator": [cv.boolean], + "description": "A list of boolean values.", + "example": "[True, False]", + "selector": {"object": {}}, + }, + UserServiceArgType.INT_ARRAY: { + "validator": [vol.Coerce(int)], + "description": "A list of integer values.", + "example": "[42, 34]", + "selector": {"object": {}}, + }, + UserServiceArgType.FLOAT_ARRAY: { + "validator": [vol.Coerce(float)], + "description": "A list of floating point numbers.", + "example": "[ 12.3, 34.5 ]", + "selector": {"object": {}}, + }, + UserServiceArgType.STRING_ARRAY: { + "validator": [cv.string], + "description": "A list of strings.", + "example": "['Example text', 'Another example']", + "selector": {"object": {}}, + }, }[arg.type_] + schema[vol.Required(arg.name)] = metadata["validator"] + fields[arg.name] = { + "name": arg.name, + "required": True, + "description": metadata.get("description"), + "example": metadata["example"], + "selector": metadata["selector"], + } async def execute_service(call): await entry_data.client.execute_service(service, call.data) @@ -323,9 +540,16 @@ async def _register_service( DOMAIN, service_name, execute_service, vol.Schema(schema) ) + service_desc = { + "description": f"Calls the service {service.name} of the node {entry_data.device_info.name}", + "fields": fields, + } + + async_set_service_schema(hass, DOMAIN, service_name, service_desc) + async def _setup_services( - hass: HomeAssistantType, entry_data: RuntimeEntryData, services: List[UserService] + hass: HomeAssistantType, entry_data: RuntimeEntryData, services: list[UserService] ): old_services = entry_data.services.copy() to_unregister = [] @@ -360,8 +584,6 @@ async def _cleanup_instance( ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" data: RuntimeEntryData = hass.data[DOMAIN].pop(entry.entry_id) - if data.reconnect_task is not None: - data.reconnect_task.cancel() for disconnect_cb in data.disconnect_callbacks: disconnect_cb() for cleanup_callback in data.cleanup_callbacks: @@ -402,7 +624,7 @@ async def platform_async_setup_entry( entry_data.state[component_key] = {} @callback - def async_list_entities(infos: List[EntityInfo]): + def async_list_entities(infos: list[EntityInfo]): """Update entities of this platform when entities are listed.""" old_infos = entry_data.info[component_key] new_infos = {} @@ -476,7 +698,7 @@ def esphome_state_property(func): class EsphomeEnumMapper: """Helper class to convert between hass and esphome enum values.""" - def __init__(self, func: Callable[[], Dict[int, str]]): + def __init__(self, func: Callable[[], dict[int, str]]): """Construct a EsphomeEnumMapper.""" self._func = func @@ -490,7 +712,7 @@ class EsphomeEnumMapper: return inverse[value] -def esphome_map_enum(func: Callable[[], Dict[int, str]]): +def esphome_map_enum(func: Callable[[], dict[int, str]]): """Map esphome int enum values to hass string constants. This class has to be used as a decorator. This ensures the aioesphomeapi @@ -562,7 +784,7 @@ class EsphomeBaseEntity(Entity): return self._entry_data.client @property - def _state(self) -> Optional[EntityState]: + def _state(self) -> EntityState | None: try: return self._entry_data.state[self._component_key][self._key] except KeyError: @@ -581,14 +803,14 @@ class EsphomeBaseEntity(Entity): return self._entry_data.available @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique id identifying the entity.""" if not self._static_info.unique_id: return None return self._static_info.unique_id @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device registry information for this entity.""" return { "connections": {(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index d605a48410b..28cc47691f5 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,5 +1,5 @@ """Support for ESPHome binary sensors.""" -from typing import Optional +from __future__ import annotations from aioesphomeapi import BinarySensorInfo, BinarySensorState @@ -29,11 +29,11 @@ class EsphomeBinarySensor(EsphomeEntity, BinarySensorEntity): return super()._static_info @property - def _state(self) -> Optional[BinarySensorState]: + def _state(self) -> BinarySensorState | None: return super()._state @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" if self._static_info.is_status_binary_sensor: # Status binary sensors indicated connected state. diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 5b8f4f0d7e6..c868d7b320a 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -1,6 +1,7 @@ """Support for ESPHome cameras.""" +from __future__ import annotations + import asyncio -from typing import Optional from aioesphomeapi import CameraInfo, CameraState @@ -42,7 +43,7 @@ class EsphomeCamera(Camera, EsphomeBaseEntity): return super()._static_info @property - def _state(self) -> Optional[CameraState]: + def _state(self) -> CameraState | None: return super()._state async def async_added_to_hass(self) -> None: @@ -67,7 +68,7 @@ class EsphomeCamera(Camera, EsphomeBaseEntity): async with self._image_cond: self._image_cond.notify_all() - async def async_camera_image(self) -> Optional[bytes]: + async def async_camera_image(self) -> bytes | None: """Return single camera image bytes.""" if not self.available: return None @@ -78,7 +79,7 @@ class EsphomeCamera(Camera, EsphomeBaseEntity): return None return self._state.image[:] - async def _async_camera_stream_image(self) -> Optional[bytes]: + async def _async_camera_stream_image(self) -> bytes | None: """Return a single camera image in a stream.""" if not self.available: return None diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index fe171d24fb9..5d21d495ec2 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -1,5 +1,5 @@ """Support for ESPHome climate devices.""" -from typing import List, Optional +from __future__ import annotations from aioesphomeapi import ( ClimateAction, @@ -134,7 +134,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): return super()._static_info @property - def _state(self) -> Optional[ClimateState]: + def _state(self) -> ClimateState | None: return super()._state @property @@ -153,7 +153,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): return TEMP_CELSIUS @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available operation modes.""" return [ _climate_modes.from_esphome(mode) @@ -217,12 +217,12 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): # pylint: disable=invalid-overridden-method @esphome_state_property - def hvac_mode(self) -> Optional[str]: + def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" return _climate_modes.from_esphome(self._state.mode) @esphome_state_property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return current action.""" # HA has no support feature field for hvac_action if not self._static_info.supports_action: @@ -245,22 +245,22 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity): return _swing_modes.from_esphome(self._state.swing_mode) @esphome_state_property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._state.current_temperature @esphome_state_property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._state.target_temperature @esphome_state_property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._state.target_temperature_low @esphome_state_property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" return self._state.target_temperature_high diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index a84aa2959ea..45a33a0dc24 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure esphome component.""" +from __future__ import annotations + from collections import OrderedDict -from typing import Optional from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol @@ -23,12 +24,12 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" - self._host: Optional[str] = None - self._port: Optional[int] = None - self._password: Optional[str] = None + self._host: str | None = None + self._port: int | None = None + self._password: str | None = None async def async_step_user( - self, user_input: Optional[ConfigType] = None, error: Optional[str] = None + self, user_input: ConfigType | None = None, error: str | None = None ): # pylint: disable=arguments-differ """Handle a flow initialized by the user.""" if user_input is not None: diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 4c42fe0d522..294689d075a 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -1,5 +1,5 @@ """Support for ESPHome covers.""" -from typing import Optional +from __future__ import annotations from aioesphomeapi import CoverInfo, CoverOperation, CoverState @@ -64,14 +64,14 @@ class EsphomeCover(EsphomeEntity, CoverEntity): return self._static_info.assumed_state @property - def _state(self) -> Optional[CoverState]: + def _state(self) -> CoverState | None: return super()._state # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property # pylint: disable=invalid-overridden-method @esphome_state_property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" # Check closed state with api version due to a protocol change return self._state.is_closed(self._client.api_version) @@ -87,14 +87,14 @@ class EsphomeCover(EsphomeEntity, CoverEntity): return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property - def current_cover_position(self) -> Optional[int]: + def current_cover_position(self) -> int | None: """Return current position of cover. 0 is closed, 100 is open.""" if not self._static_info.supports_position: return None return round(self._state.position * 100.0) @esphome_state_property - def current_cover_tilt_position(self) -> Optional[float]: + def current_cover_tilt_position(self) -> float | None: """Return current position of cover tilt. 0 is closed, 100 is open.""" if not self._static_info.supports_tilt: return None diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 58b20d18e12..34ed6ffee46 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -1,6 +1,8 @@ """Runtime entry data for ESPHome stored in hass.data.""" +from __future__ import annotations + import asyncio -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Callable from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, @@ -50,24 +52,23 @@ class RuntimeEntryData: """Store runtime data for esphome config entries.""" entry_id: str = attr.ib() - client: "APIClient" = attr.ib() + client: APIClient = attr.ib() store: Store = attr.ib() - reconnect_task: Optional[asyncio.Task] = attr.ib(default=None) - state: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) - info: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) + state: dict[str, dict[str, Any]] = attr.ib(factory=dict) + info: dict[str, dict[str, Any]] = attr.ib(factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires # some static info to be accessible during removal (unique_id, maybe others) # If an entity can't find anything in the info array, it will look for info here. - old_info: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) + old_info: dict[str, dict[str, Any]] = attr.ib(factory=dict) - services: Dict[int, "UserService"] = attr.ib(factory=dict) + services: dict[int, UserService] = attr.ib(factory=dict) available: bool = attr.ib(default=False) - device_info: Optional[DeviceInfo] = attr.ib(default=None) - cleanup_callbacks: List[Callable[[], None]] = attr.ib(factory=list) - disconnect_callbacks: List[Callable[[], None]] = attr.ib(factory=list) - loaded_platforms: Set[str] = attr.ib(factory=set) + device_info: DeviceInfo | None = attr.ib(default=None) + cleanup_callbacks: list[Callable[[], None]] = attr.ib(factory=list) + disconnect_callbacks: list[Callable[[], None]] = attr.ib(factory=list) + loaded_platforms: set[str] = attr.ib(factory=set) platform_load_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock) @callback @@ -87,7 +88,7 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal) async def _ensure_platforms_loaded( - self, hass: HomeAssistantType, entry: ConfigEntry, platforms: Set[str] + self, hass: HomeAssistantType, entry: ConfigEntry, platforms: set[str] ): async with self.platform_load_lock: needed = platforms - self.loaded_platforms @@ -101,7 +102,7 @@ class RuntimeEntryData: self.loaded_platforms |= needed async def async_update_static_infos( - self, hass: HomeAssistantType, entry: ConfigEntry, infos: List[EntityInfo] + self, hass: HomeAssistantType, entry: ConfigEntry, infos: list[EntityInfo] ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms @@ -129,7 +130,7 @@ class RuntimeEntryData: signal = f"esphome_{self.entry_id}_on_device_update" async_dispatcher_send(hass, signal) - async def async_load_from_store(self) -> Tuple[List[EntityInfo], List[UserService]]: + async def async_load_from_store(self) -> tuple[list[EntityInfo], list[UserService]]: """Load the retained data from store and return de-serialized data.""" restored = await self.store.async_load() if restored is None: diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index df23f37cb63..5d7cf24f2c5 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -1,5 +1,7 @@ """Support for ESPHome fans.""" -from typing import Optional +from __future__ import annotations + +import math from aioesphomeapi import FanDirection, FanInfo, FanSpeed, FanState @@ -16,6 +18,8 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, ) from . import ( @@ -59,9 +63,14 @@ class EsphomeFan(EsphomeEntity, FanEntity): return super()._static_info @property - def _state(self) -> Optional[FanState]: + def _state(self) -> FanState | None: return super()._state + @property + def _supports_speed_levels(self) -> bool: + api_version = self._client.api_version + return api_version.major == 1 and api_version.minor > 3 + async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" if percentage == 0: @@ -70,17 +79,24 @@ class EsphomeFan(EsphomeEntity, FanEntity): data = {"key": self._static_info.key, "state": True} if percentage is not None: - named_speed = percentage_to_ordered_list_item( - ORDERED_NAMED_FAN_SPEEDS, percentage - ) - data["speed"] = named_speed + if self._supports_speed_levels: + data["speed_level"] = math.ceil( + percentage_to_ranged_value( + (1, self._static_info.supported_speed_levels), percentage + ) + ) + else: + named_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + data["speed"] = named_speed await self._client.fan_command(**data) async def async_turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" @@ -106,23 +122,31 @@ class EsphomeFan(EsphomeEntity, FanEntity): # pylint: disable=invalid-overridden-method @esphome_state_property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if the entity is on.""" return self._state.state @esphome_state_property - def percentage(self) -> Optional[str]: + def percentage(self) -> int | None: """Return the current speed percentage.""" if not self._static_info.supports_speed: return None - return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, self._state.speed + + if not self._supports_speed_levels: + return ordered_list_item_to_percentage( + ORDERED_NAMED_FAN_SPEEDS, self._state.speed + ) + + return ranged_value_to_percentage( + (1, self._static_info.supported_speed_levels), self._state.speed_level ) @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return len(ORDERED_NAMED_FAN_SPEEDS) + if not self._supports_speed_levels: + return len(ORDERED_NAMED_FAN_SPEEDS) + return self._static_info.supported_speed_levels @esphome_state_property def oscillating(self) -> None: diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index adbb36188fd..29fd969d479 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -1,5 +1,5 @@ """Support for ESPHome lights.""" -from typing import List, Optional, Tuple +from __future__ import annotations from aioesphomeapi import LightInfo, LightState @@ -54,14 +54,14 @@ class EsphomeLight(EsphomeEntity, LightEntity): return super()._static_info @property - def _state(self) -> Optional[LightState]: + def _state(self) -> LightState | None: return super()._state # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property # pylint: disable=invalid-overridden-method @esphome_state_property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if the switch is on.""" return self._state.state @@ -96,29 +96,29 @@ class EsphomeLight(EsphomeEntity, LightEntity): await self._client.light_command(**data) @esphome_state_property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return round(self._state.brightness * 255) @esphome_state_property - def hs_color(self) -> Optional[Tuple[float, float]]: + def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" return color_util.color_RGB_to_hs( self._state.red * 255, self._state.green * 255, self._state.blue * 255 ) @esphome_state_property - def color_temp(self) -> Optional[float]: + def color_temp(self) -> float | None: """Return the CT color value in mireds.""" return self._state.color_temperature @esphome_state_property - def white_value(self) -> Optional[int]: + def white_value(self) -> int | None: """Return the white value of this light between 0..255.""" return round(self._state.white * 255) @esphome_state_property - def effect(self) -> Optional[str]: + def effect(self) -> str | None: """Return the current effect.""" return self._state.effect @@ -140,7 +140,7 @@ class EsphomeLight(EsphomeEntity, LightEntity): return flags @property - def effect_list(self) -> List[str]: + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._static_info.effects diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 17ff2ca96ba..e3c609c9fad 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.6.5"], + "requirements": ["aioesphomeapi==2.6.6"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], "after_dependencies": ["zeroconf", "tag"] diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index fe9922cf0ed..d751109c159 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -1,11 +1,12 @@ """Support for esphome sensors.""" +from __future__ import annotations + import math -from typing import Optional from aioesphomeapi import SensorInfo, SensorState, TextSensorInfo, TextSensorState import voluptuous as vol -from homeassistant.components.sensor import DEVICE_CLASSES +from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.config_entries import ConfigEntry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType @@ -41,7 +42,7 @@ async def async_setup_entry( # pylint: disable=invalid-overridden-method -class EsphomeSensor(EsphomeEntity): +class EsphomeSensor(EsphomeEntity, SensorEntity): """A sensor implementation for esphome.""" @property @@ -49,7 +50,7 @@ class EsphomeSensor(EsphomeEntity): return super()._static_info @property - def _state(self) -> Optional[SensorState]: + def _state(self) -> SensorState | None: return super()._state @property @@ -65,7 +66,7 @@ class EsphomeSensor(EsphomeEntity): return self._static_info.force_update @esphome_state_property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the entity.""" if math.isnan(self._state.state): return None @@ -88,7 +89,7 @@ class EsphomeSensor(EsphomeEntity): return self._static_info.device_class -class EsphomeTextSensor(EsphomeEntity): +class EsphomeTextSensor(EsphomeEntity, SensorEntity): """A text sensor implementation for ESPHome.""" @property @@ -96,7 +97,7 @@ class EsphomeTextSensor(EsphomeEntity): return super()._static_info @property - def _state(self) -> Optional[TextSensorState]: + def _state(self) -> TextSensorState | None: return super()._state @property @@ -105,7 +106,7 @@ class EsphomeTextSensor(EsphomeEntity): return self._static_info.icon @esphome_state_property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the entity.""" if self._state.missing_state: return None diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 3f8ca90d6c5..992f014e829 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -1,5 +1,5 @@ """Support for ESPHome switches.""" -from typing import Optional +from __future__ import annotations from aioesphomeapi import SwitchInfo, SwitchState @@ -33,7 +33,7 @@ class EsphomeSwitch(EsphomeEntity, SwitchEntity): return super()._static_info @property - def _state(self) -> Optional[SwitchState]: + def _state(self) -> SwitchState | None: return super()._state @property @@ -49,7 +49,7 @@ class EsphomeSwitch(EsphomeEntity, SwitchEntity): # https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property # pylint: disable=invalid-overridden-method @esphome_state_property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if the switch is on.""" return self._state.state diff --git a/homeassistant/components/esphome/translations/he.json b/homeassistant/components/esphome/translations/he.json new file mode 100644 index 00000000000..648d007cc46 --- /dev/null +++ b/homeassistant/components/esphome/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "authenticate": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 46cb3228edb..6c4586fbd55 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Az ESP-t m\u00e1r konfigur\u00e1ltad." + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van." }, "error": { "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rlek gy\u0151z\u0151dj meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rlek, \u00e1ll\u00edts be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index 9f6cd012949..a39a19e12db 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -1,14 +1,32 @@ { "config": { "abort": { - "already_configured": "ESP sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung" }, + "error": { + "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", + "invalid_auth": "Autentikasi tidak valid", + "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { - "password": "Kata kunci" + "password": "Kata Sandi" }, - "description": "Silakan masukkan kata kunci yang Anda atur di konfigurasi Anda." + "description": "Masukkan kata sandi yang ditetapkan di konfigurasi Anda untuk {name}." + }, + "discovery_confirm": { + "description": "Ingin menambahkan node ESPHome `{name}` ke Home Assistant?", + "title": "Perangkat node ESPHome yang ditemukan" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Masukkan pengaturan koneksi node [ESPHome](https://esphomelib.com/)." } } } diff --git a/homeassistant/components/esphome/translations/ko.json b/homeassistant/components/esphome/translations/ko.json index 18827f69024..3e98bdabeb5 100644 --- a/homeassistant/components/esphome/translations/ko.json +++ b/homeassistant/components/esphome/translations/ko.json @@ -5,7 +5,7 @@ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4" }, "error": { - "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "connection_error": "ESP\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:'\ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, @@ -18,7 +18,7 @@ "description": "{name} \uc758 \uad6c\uc131\uc5d0 \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." }, "discovery_confirm": { - "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Home Assistant\uc5d0 ESPHome node `{name}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ubc1c\uacac\ub41c ESPHome node" }, "user": { diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index f32dbd1723e..1aae006feed 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "ESP is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al begonnen" }, "error": { diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index 3ac7af315b7..f0dc70d7be4 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -1,14 +1,14 @@ """Support for Essent API.""" +from __future__ import annotations + from datetime import timedelta -from typing import Optional from pyessent import PyEssent import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle SCAN_INTERVAL = timedelta(hours=1) @@ -81,7 +81,7 @@ class EssentBase: self._meter_data[possible_meter] = meter_data -class EssentMeter(Entity): +class EssentMeter(SensorEntity): """Representation of Essent measurements.""" def __init__(self, essent_base, meter, meter_type, tariff, unit): @@ -94,7 +94,7 @@ class EssentMeter(Entity): self._unit = unit @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return f"{self._meter}-{self._type}-{self._tariff}" diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 1c14ce578c1..1fa2edbf2e8 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -4,10 +4,9 @@ from datetime import timedelta from pyetherscan import get_balance import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTRIBUTION = "Data provided by etherscan.io" @@ -42,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EtherscanSensor(name, address, token, token_address)], True) -class EtherscanSensor(Entity): +class EtherscanSensor(SensorEntity): """Representation of an Etherscan.io sensor.""" def __init__(self, name, address, token, token_address): @@ -70,7 +69,7 @@ class EtherscanSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 243bd2913b1..e451b83c1fa 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -1,7 +1,8 @@ """Support for EverLights lights.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Tuple import pyeverlights import voluptuous as vol @@ -38,7 +39,7 @@ def color_rgb_to_int(red: int, green: int, blue: int) -> int: return red * 256 * 256 + green * 256 + blue -def color_int_to_rgb(value: int) -> Tuple[int, int, int]: +def color_int_to_rgb(value: int) -> tuple[int, int, int]: """Return an RGB tuple from an integer.""" return (value >> 16, (value >> 8) & 0xFF, value & 0xFF) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 268e7709af3..8c83308a8b7 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -2,10 +2,12 @@ Such systems include evohome, Round Thermostat, and others. """ +from __future__ import annotations + from datetime import datetime as dt, timedelta import logging import re -from typing import Any, Dict, Optional, Tuple +from typing import Any import aiohttp.client_exceptions import evohomeasync @@ -114,7 +116,7 @@ def convert_until(status_dict: dict, until_key: str) -> None: status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() -def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]: +def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: """Recursively convert a dict's keys to snake_case.""" def convert_key(key: str) -> str: @@ -176,7 +178,7 @@ def _handle_exception(err) -> bool: async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell TCC system.""" - async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]: + async def load_auth_tokens(store) -> tuple[dict, dict | None]: app_storage = await store.async_load() tokens = dict(app_storage if app_storage else {}) @@ -435,7 +437,7 @@ class EvoBroker: async def _update_v1_api_temps(self, *args, **kwargs) -> None: """Get the latest high-precision temperatures of the default Location.""" - def get_session_id(client_v1) -> Optional[str]: + def get_session_id(client_v1) -> str | None: user_data = client_v1.user_data if client_v1 else None return user_data.get("sessionId") if user_data else None @@ -520,7 +522,7 @@ class EvoDevice(Entity): self._supported_features = None self._device_state_attrs = {} - async def async_refresh(self, payload: Optional[dict] = None) -> None: + async def async_refresh(self, payload: dict | None = None) -> None: """Process any signals.""" if payload is None: self.async_schedule_update_ha_state(force_refresh=True) @@ -546,7 +548,7 @@ class EvoDevice(Entity): return False @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._unique_id @@ -556,7 +558,7 @@ class EvoDevice(Entity): return self._name @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the evohome-specific state attributes.""" status = self._device_state_attrs if "systemModeStatus" in status: @@ -606,7 +608,7 @@ class EvoChild(EvoDevice): self._setpoints = {} @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature of a Zone.""" if ( self._evo_broker.temps @@ -618,7 +620,7 @@ class EvoChild(EvoDevice): return self._evo_device.temperatureStatus["temperature"] @property - def setpoints(self) -> Dict[str, Any]: + def setpoints(self) -> dict[str, Any]: """Return the current/next setpoints from the schedule. Only Zones & DHW controllers (but not the TCS) can have schedules. diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e99cae5e22e..f291fcd9cb3 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,7 +1,8 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" +from __future__ import annotations + from datetime import datetime as dt import logging -from typing import List, Optional from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -129,12 +130,12 @@ class EvoClimateEntity(EvoDevice, ClimateEntity): self._preset_modes = None @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return self._preset_modes @@ -203,7 +204,7 @@ class EvoZone(EvoChild, EvoClimateEntity): return self._evo_device.setpointStatus["targetHeatTemperature"] @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) @@ -268,7 +269,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.cancel_temp_override() ) - async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: str | None) -> None: """Set the preset mode; if None, then revert to following the schedule.""" evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) @@ -347,7 +348,7 @@ class EvoController(EvoClimateEntity): await self._set_tcs_mode(mode, until=until) - async def _set_tcs_mode(self, mode: str, until: Optional[dt] = None) -> None: + async def _set_tcs_mode(self, mode: str, until: dt | None = None) -> None: """Set a Controller to any of its native EVO_* operating modes.""" until = dt_util.as_utc(until) if until else None await self._evo_broker.call_client_api( @@ -361,7 +362,7 @@ class EvoController(EvoClimateEntity): return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the average current temperature of the heating Zones. Controllers do not have a current temp, but one is expected by HA. @@ -374,7 +375,7 @@ class EvoController(EvoClimateEntity): return round(sum(temps) / len(temps), 1) if temps else None @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) @@ -396,7 +397,7 @@ class EvoController(EvoClimateEntity): """Set an operating mode for a Controller.""" await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: str | None) -> None: """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 8bcecca551b..e707387ce4f 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -2,6 +2,6 @@ "domain": "evohome", "name": "Honeywell Total Connect Comfort (Europe)", "documentation": "https://www.home-assistant.io/integrations/evohome", - "requirements": ["evohome-async==0.3.5.post1"], + "requirements": ["evohome-async==0.3.8"], "codeowners": ["@zxdavb"] } diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 846c8c09155..4e05c553461 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -1,6 +1,7 @@ """Support for WaterHeater devices of (EMEA/EU) Honeywell TCC systems.""" +from __future__ import annotations + import logging -from typing import List from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, @@ -70,7 +71,7 @@ class EvoDHW(EvoChild, WaterHeaterEntity): return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property - def operation_list(self) -> List[str]: + def operation_list(self) -> list[str]: """Return the list of available operations.""" return list(HA_STATE_TO_EVO) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index b9b2463314b..4cce0e68654 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -158,7 +158,7 @@ class HassEzvizCamera(Camera): return True @property - def device_state_attributes(self): + 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 diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index b9def765123..2669105469e 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -10,7 +10,6 @@ from faadelays import Airport from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -32,16 +31,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): code = entry.data[CONF_ID] coordinator = FAADataUpdateCoordinator(hass, code) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -52,8 +48,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py index 6c5876b7017..b96ee24a5bc 100644 --- a/homeassistant/components/faa_delays/binary_sensor.py +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -68,7 +68,7 @@ class FAABinarySensor(CoordinatorEntity, BinarySensorEntity): return f"{self._id}_{self._sensor_type}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return attributes for sensor.""" if self._sensor_type == "GROUND_DELAY": self._attrs["average"] = self.coordinator.data.ground_delay.average diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py index 46d917cc92f..b77ab3554b5 100644 --- a/homeassistant/components/faa_delays/config_flow.py +++ b/homeassistant/components/faa_delays/config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_ID from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/faa_delays/manifest.json b/homeassistant/components/faa_delays/manifest.json index 4148e7b956f..7ffe7898b60 100644 --- a/homeassistant/components/faa_delays/manifest.json +++ b/homeassistant/components/faa_delays/manifest.json @@ -2,7 +2,7 @@ "domain": "faa_delays", "name": "FAA Delays", "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/faadelays", + "documentation": "https://www.home-assistant.io/integrations/faa_delays", "requirements": ["faadelays==0.0.6"], "codeowners": ["@ntilley905"] } diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json new file mode 100644 index 00000000000..0995436221b --- /dev/null +++ b/homeassistant/components/faa_delays/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "id": "\u041b\u0435\u0442\u0438\u0449\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/de.json b/homeassistant/components/faa_delays/translations/de.json index 72b837c862c..9519c7d4470 100644 --- a/homeassistant/components/faa_delays/translations/de.json +++ b/homeassistant/components/faa_delays/translations/de.json @@ -1,8 +1,21 @@ { "config": { + "abort": { + "already_configured": "Dieser Flughafen ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_airport": "Flughafencode ist ung\u00fcltig", "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "id": "Flughafen" + }, + "description": "Geben Sie einen US-Flughafencode im IATA-Format ein", + "title": "FAA Delays" + } } } } \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/es.json b/homeassistant/components/faa_delays/translations/es.json index 94eca99dda3..71f7fecef41 100644 --- a/homeassistant/components/faa_delays/translations/es.json +++ b/homeassistant/components/faa_delays/translations/es.json @@ -4,7 +4,9 @@ "already_configured": "Este aeropuerto ya est\u00e1 configurado." }, "error": { - "invalid_airport": "El c\u00f3digo del aeropuerto no es v\u00e1lido" + "cannot_connect": "Fallo al conectar", + "invalid_airport": "El c\u00f3digo del aeropuerto no es v\u00e1lido", + "unknown": "Error inesperado" }, "step": { "user": { diff --git a/homeassistant/components/faa_delays/translations/hu.json b/homeassistant/components/faa_delays/translations/hu.json new file mode 100644 index 00000000000..c511f42a726 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a rep\u00fcl\u0151t\u00e9r m\u00e1r konfigur\u00e1lva van." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_airport": "A rep\u00fcl\u0151t\u00e9r k\u00f3dja \u00e9rv\u00e9nytelen", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "id": "Rep\u00fcl\u0151t\u00e9r" + }, + "description": "Amerikai rep\u00fcl\u0151t\u00e9ri k\u00f3d be\u00edr\u00e1sa IATA form\u00e1tumban", + "title": "FAA Delays" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/id.json b/homeassistant/components/faa_delays/translations/id.json new file mode 100644 index 00000000000..4f4c3a93924 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Bandara ini sudah dikonfigurasi." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_airport": "Kode bandara tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "id": "Bandara" + }, + "description": "Masukkan Kode Bandara AS dalam Format IATA", + "title": "Penundaan FAA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/ko.json b/homeassistant/components/faa_delays/translations/ko.json new file mode 100644 index 00000000000..2d755e5de28 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uacf5\ud56d\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_airport": "\uacf5\ud56d \ucf54\ub4dc\uac00 \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": { + "id": "\uacf5\ud56d" + }, + "description": "IATA \ud615\uc2dd\uc758 \ubbf8\uad6d \uacf5\ud56d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "FAA \ud56d\uacf5 \uc5f0\ucc29 \uc815\ubcf4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/faa_delays/translations/pl.json b/homeassistant/components/faa_delays/translations/pl.json new file mode 100644 index 00000000000..7073597f529 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "To lotnisko jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_airport": "Kod lotniska jest nieprawid\u0142owy", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "id": "Lotnisko" + }, + "description": "Wprowad\u017a kod lotniska w Stanach w formacie IATA", + "title": "Op\u00f3\u017anienia FAA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/pt-BR.json b/homeassistant/components/faa_delays/translations/pt.json similarity index 58% rename from homeassistant/components/griddy/translations/pt-BR.json rename to homeassistant/components/faa_delays/translations/pt.json index dc9c1362dc4..49cb628dd85 100644 --- a/homeassistant/components/griddy/translations/pt-BR.json +++ b/homeassistant/components/faa_delays/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/faa_delays/translations/zh-Hans.json b/homeassistant/components/faa_delays/translations/zh-Hans.json new file mode 100644 index 00000000000..4052f12f524 --- /dev/null +++ b/homeassistant/components/faa_delays/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_airport": "\u822a\u73ed\u53f7\u65e0\u6548", + "unknown": "\u9884\u671f\u5916\u7684\u9519\u8bef" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 6a460ac305b..5c90ce73560 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -263,7 +263,7 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the classifier attributes.""" return { "matched_faces": self._matched, diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 2f206dca737..29ac5c3d0b5 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -6,10 +6,9 @@ import re import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_FILE_PATH, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -45,7 +44,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(device_list, True) -class BanSensor(Entity): +class BanSensor(SensorEntity): """Implementation of a fail2ban sensor.""" def __init__(self, name, jail, log_parser): @@ -66,7 +65,7 @@ class BanSensor(Entity): return self._name @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the fail2ban sensor.""" return self.ban_dict @@ -91,9 +90,11 @@ class BanSensor(Entity): if len(self.ban_dict[STATE_ALL_BANS]) > 10: self.ban_dict[STATE_ALL_BANS].pop(0) - elif entry[0] == "Unban": - if current_ip in self.ban_dict[STATE_CURRENT_BANS]: - self.ban_dict[STATE_CURRENT_BANS].remove(current_ip) + elif ( + entry[0] == "Unban" + and current_ip in self.ban_dict[STATE_CURRENT_BANS] + ): + self.ban_dict[STATE_CURRENT_BANS].remove(current_ip) if self.ban_dict[STATE_CURRENT_BANS]: self.last_ban = self.ban_dict[STATE_CURRENT_BANS][-1] diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 1fbf35b1603..f484ca36b25 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -1,9 +1,11 @@ """Provides functionality to interact with fans.""" +from __future__ import annotations + from datetime import timedelta import functools as ft import logging import math -from typing import List, Optional +from typing import final import voluptuous as vol @@ -219,7 +221,7 @@ def _fan_native(method): class FanEntity(ToggleEntity): - """Representation of a fan.""" + """Base class for fan entities.""" @_fan_native def set_speed(self, speed: str) -> None: @@ -229,7 +231,7 @@ class FanEntity(ToggleEntity): async def async_set_speed_deprecated(self, speed: str): """Set the speed of the fan.""" _LOGGER.warning( - "fan.set_speed is deprecated, use fan.set_percentage or fan.set_preset_mode instead." + "The fan.set_speed service is deprecated, use fan.set_percentage or fan.set_preset_mode instead" ) await self.async_set_speed(speed) @@ -274,16 +276,16 @@ class FanEntity(ToggleEntity): else: await self.async_set_speed(self.percentage_to_speed(percentage)) - async def async_increase_speed(self, percentage_step: Optional[int] = None) -> None: + async def async_increase_speed(self, percentage_step: int | None = None) -> None: """Increase the speed of the fan.""" await self._async_adjust_speed(1, percentage_step) - async def async_decrease_speed(self, percentage_step: Optional[int] = None) -> None: + async def async_decrease_speed(self, percentage_step: int | None = None) -> None: """Decrease the speed of the fan.""" await self._async_adjust_speed(-1, percentage_step) async def _async_adjust_speed( - self, modifier: int, percentage_step: Optional[int] + self, modifier: int, percentage_step: int | None ) -> None: """Increase or decrease the speed of the fan.""" current_percentage = self.percentage or 0 @@ -338,20 +340,19 @@ class FanEntity(ToggleEntity): # pylint: disable=arguments-differ def turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" raise NotImplementedError() - # pylint: disable=arguments-differ async def async_turn_on_compat( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan. @@ -368,7 +369,7 @@ class FanEntity(ToggleEntity): percentage = None elif speed is not None: _LOGGER.warning( - "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead." + "Calling fan.turn_on with the speed argument is deprecated, use percentage or preset_mode instead" ) if speed in self.preset_modes: preset_mode = speed @@ -388,9 +389,9 @@ class FanEntity(ToggleEntity): # pylint: disable=arguments-differ async def async_turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" @@ -421,28 +422,28 @@ class FanEntity(ToggleEntity): return self.speed not in [SPEED_OFF, None] @property - def _implemented_percentage(self): + def _implemented_percentage(self) -> bool: """Return true if percentage has been implemented.""" return not hasattr(self.set_percentage, _FAN_NATIVE) or not hasattr( self.async_set_percentage, _FAN_NATIVE ) @property - def _implemented_preset_mode(self): + def _implemented_preset_mode(self) -> bool: """Return true if preset_mode has been implemented.""" return not hasattr(self.set_preset_mode, _FAN_NATIVE) or not hasattr( self.async_set_preset_mode, _FAN_NATIVE ) @property - def _implemented_speed(self): + def _implemented_speed(self) -> bool: """Return true if speed has been implemented.""" return not hasattr(self.set_speed, _FAN_NATIVE) or not hasattr( self.async_set_speed, _FAN_NATIVE ) @property - def speed(self) -> Optional[str]: + def speed(self) -> str | None: """Return the current speed.""" if self._implemented_preset_mode: preset_mode = self.preset_mode @@ -456,11 +457,10 @@ class FanEntity(ToggleEntity): return None @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the current speed as a percentage.""" - if not self._implemented_preset_mode: - if self.speed in self.preset_modes: - return None + if not self._implemented_preset_mode and self.speed in self.preset_modes: + return None if not self._implemented_percentage: return self.speed_to_percentage(self.speed) return 0 @@ -489,7 +489,7 @@ class FanEntity(ToggleEntity): return speeds @property - def current_direction(self) -> Optional[str]: + def current_direction(self) -> str | None: """Return the current direction of the fan.""" return None @@ -586,6 +586,7 @@ class FanEntity(ToggleEntity): f"The speed_list {speed_list} does not contain any valid speeds." ) from ex + @final @property def state_attributes(self) -> dict: """Return optional state attributes.""" @@ -617,7 +618,7 @@ class FanEntity(ToggleEntity): return 0 @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., auto, smart, interval, favorite. Requires SUPPORT_SET_SPEED. @@ -628,7 +629,7 @@ class FanEntity(ToggleEntity): return None @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. Requires SUPPORT_SET_SPEED. @@ -636,7 +637,7 @@ class FanEntity(ToggleEntity): return preset_modes_from_speed_list(self.speed_list) -def speed_list_without_preset_modes(speed_list: List): +def speed_list_without_preset_modes(speed_list: list): """Filter out non-speeds from the speed list. The goal is to get the speeds in a list from lowest to @@ -660,7 +661,7 @@ def speed_list_without_preset_modes(speed_list: List): return [speed for speed in speed_list if speed.lower() not in _NOT_SPEEDS_FILTER] -def preset_modes_from_speed_list(speed_list: List): +def preset_modes_from_speed_list(speed_list: list): """Filter out non-preset modes from the speed list. The goal is to return only preset modes. diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index a5d35d741b6..f4611d353d5 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Fan.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Fan devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -59,11 +59,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if config[CONF_TYPE] == "turn_on": diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index d3a8aa5c395..9aa9620ef72 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -1,5 +1,5 @@ """Provide the device automations for Fan.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -32,7 +32,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Fan devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 95f4b429a24..15f8f4be45e 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -1,67 +1,29 @@ """Provides device automations for Fan.""" -from typing import List +from __future__ import annotations import voluptuous as vol from homeassistant.components.automation import AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA -from homeassistant.components.homeassistant.triggers import state as state_trigger -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, - STATE_OFF, - STATE_ON, -) +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType from . import DOMAIN -TRIGGER_TYPES = {"turned_on", "turned_off"} - -TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( - { - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), - } +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Fan devices.""" - registry = await entity_registry.async_get_registry(hass) - triggers = [] + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) - # Get all the integrations entities for this device - for entry in entity_registry.async_entries_for_device(registry, device_id): - if entry.domain != DOMAIN: - continue - # Add triggers for each entity that belongs to this integration - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turned_on", - } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "turned_off", - } - ) - - return triggers +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, config) async def async_attach_trigger( @@ -70,20 +32,7 @@ async def async_attach_trigger( action: AutomationActionType, automation_info: dict, ) -> CALLBACK_TYPE: - """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - - if config[CONF_TYPE] == "turned_on": - to_state = STATE_ON - else: - to_state = STATE_OFF - - state_config = { - state_trigger.CONF_PLATFORM: "state", - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state_trigger.CONF_TO: to_state, - } - state_config = state_trigger.TRIGGER_SCHEMA(state_config) - return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + """Listen for state changes based on configuration.""" + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info ) diff --git a/homeassistant/components/fan/group.py b/homeassistant/components/fan/group.py index 1636054663d..234883ffd5a 100644 --- a/homeassistant/components/fan/group.py +++ b/homeassistant/components/fan/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/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index b5f0fca47b7..c9da43ebe3a 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -1,8 +1,10 @@ """Reproduce an Fan state.""" +from __future__ import annotations + import asyncio import logging from types import MappingProxyType -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -11,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 ( ATTR_DIRECTION, @@ -41,11 +42,11 @@ ATTRIBUTES = { # attribute: service async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -95,11 +96,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Fan states.""" await asyncio.gather( diff --git a/homeassistant/components/fan/translations/id.json b/homeassistant/components/fan/translations/id.json index b5324f36f6a..054ec10754c 100644 --- a/homeassistant/components/fan/translations/id.json +++ b/homeassistant/components/fan/translations/id.json @@ -1,9 +1,23 @@ { - "state": { - "_": { - "off": "Off", - "on": "On" + "device_automation": { + "action_type": { + "turn_off": "Matikan {entity_name}", + "turn_on": "Nyalakan {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala" + }, + "trigger_type": { + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan" } }, - "title": "Kipas angin" + "state": { + "_": { + "off": "Mati", + "on": "Nyala" + } + }, + "title": "Kipas Angin" } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/ko.json b/homeassistant/components/fan/translations/ko.json index 5f6116d48d2..c2157f29e72 100644 --- a/homeassistant/components/fan/translations/ko.json +++ b/homeassistant/components/fan/translations/ko.json @@ -1,16 +1,16 @@ { "device_automation": { "action_type": { - "turn_off": "{entity_name} \ub044\uae30", - "turn_on": "{entity_name} \ucf1c\uae30" + "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", + "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index ad3711b1319..b4406a4de95 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -1,4 +1,5 @@ """Support for Fast.com internet speed testing sensor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,7 +15,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([SpeedtestSensor(hass.data[FASTDOTCOM_DOMAIN])]) -class SpeedtestSensor(RestoreEntity): +class SpeedtestSensor(RestoreEntity, SensorEntity): """Implementation of a FAst.com sensor.""" def __init__(self, speedtest_data): diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 4b67f06ba23..4bf8de91edc 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -1,7 +1,8 @@ """Support for FFmpeg.""" +from __future__ import annotations + import asyncio import re -from typing import Optional from haffmpeg.tools import IMAGE_JPEG, FFVersion, ImageFrame import voluptuous as vol @@ -93,7 +94,7 @@ async def async_get_image( hass: HomeAssistantType, input_source: str, output_format: str = IMAGE_JPEG, - extra_cmd: Optional[str] = None, + extra_cmd: str | None = None, ): """Get an image from a frame of an RTSP stream.""" manager = hass.data[DATA_FFMPEG] diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 51e31cf27ad..964c1112840 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -1,7 +1,8 @@ """Support for the Fibaro devices.""" +from __future__ import annotations + from collections import defaultdict import logging -from typing import Optional from fiblary3.client.v4.client import Client as FibaroClient, StateHandler import voluptuous as vol @@ -37,7 +38,7 @@ CONF_RESET_COLOR = "reset_color" DOMAIN = "fibaro" FIBARO_CONTROLLERS = "fibaro_controllers" FIBARO_DEVICES = "fibaro_devices" -FIBARO_COMPONENTS = [ +PLATFORMS = [ "binary_sensor", "climate", "cover", @@ -365,21 +366,21 @@ def setup(hass, base_config): controller.disable_state_handler() hass.data[FIBARO_DEVICES] = {} - for component in FIBARO_COMPONENTS: - hass.data[FIBARO_DEVICES][component] = [] + for platform in PLATFORMS: + hass.data[FIBARO_DEVICES][platform] = [] for gateway in gateways: controller = FibaroController(gateway) if controller.connect(): hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller - for component in FIBARO_COMPONENTS: - hass.data[FIBARO_DEVICES][component].extend( - controller.fibaro_devices[component] + for platform in PLATFORMS: + hass.data[FIBARO_DEVICES][platform].extend( + controller.fibaro_devices[platform] ) if hass.data[FIBARO_CONTROLLERS]: - for component in FIBARO_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, base_config) for controller in hass.data[FIBARO_CONTROLLERS].values(): controller.enable_state_handler() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro) @@ -496,7 +497,7 @@ class FibaroDevice(Entity): return self.fibaro_device.unique_id_str @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the device.""" return self._name @@ -506,7 +507,7 @@ class FibaroDevice(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" attr = {"fibaro_id": self.fibaro_device.id} diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 4e9af8803f2..3161e173b2a 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -1,7 +1,10 @@ """Support for Fibaro sensors.""" -from homeassistant.components.sensor import DOMAIN +from contextlib import suppress + +from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, @@ -10,7 +13,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity from . import FIBARO_DEVICES, FibaroDevice @@ -27,7 +29,13 @@ SENSOR_TYPES = { "mdi:fire", None, ], - "CO2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:cloud", None], + "CO2": [ + "CO2", + CONCENTRATION_PARTS_PER_MILLION, + None, + None, + DEVICE_CLASS_CO2, + ], "com.fibaro.humiditySensor": [ "Humidity", PERCENTAGE, @@ -48,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class FibaroSensor(FibaroDevice, Entity): +class FibaroSensor(FibaroDevice, SensorEntity): """Representation of a Fibaro Sensor.""" def __init__(self, fibaro_device): @@ -65,7 +73,7 @@ class FibaroSensor(FibaroDevice, Entity): self._unit = None self._icon = None self._device_class = None - try: + with suppress(KeyError, ValueError): if not self._unit: if self.fibaro_device.properties.unit == "lux": self._unit = LIGHT_LUX @@ -75,8 +83,6 @@ class FibaroSensor(FibaroDevice, Entity): self._unit = TEMP_FAHRENHEIT else: self._unit = self.fibaro_device.properties.unit - except (KeyError, ValueError): - pass @property def state(self): @@ -100,7 +106,5 @@ class FibaroSensor(FibaroDevice, Entity): def update(self): """Update the state.""" - try: + with suppress(KeyError, ValueError): self.current_value = float(self.fibaro_device.properties.value) - except (KeyError, ValueError): - pass diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 22522d1ab74..55ec455d8f1 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -11,7 +11,7 @@ from pyfido import FidoClient from pyfido.client import PyFidoError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -21,7 +21,6 @@ from homeassistant.const import ( TIME_MINUTES, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -90,7 +89,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, True) -class FidoSensor(Entity): +class FidoSensor(SensorEntity): """Implementation of a Fido sensor.""" def __init__(self, fido_data, sensor_type, name, number): @@ -125,7 +124,7 @@ class FidoSensor(Entity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {"number": self._number} diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 3368bd878d5..5d8a9475235 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -4,7 +4,7 @@ import os import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_FILE_PATH, CONF_NAME, @@ -12,7 +12,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -46,7 +45,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.error("'%s' is not an allowed directory", file_path) -class FileSensor(Entity): +class FileSensor(SensorEntity): """Implementation of a file sensor.""" def __init__(self, name, file_path, unit_of_measurement, value_template): diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 27122a3cb9c..856b29364ae 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -5,10 +5,9 @@ import os import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.reload import setup_reload_service from . import DOMAIN, PLATFORMS @@ -40,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class Filesize(Entity): +class Filesize(SensorEntity): """Encapsulates file size information.""" def __init__(self, path): @@ -76,7 +75,7 @@ class Filesize(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" return { "path": self._path, diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index d46709924ea..2f1705f5f4d 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -1,4 +1,6 @@ """Allows the creation of a sensor that filters state property.""" +from __future__ import annotations + from collections import Counter, deque from copy import copy from datetime import timedelta @@ -6,7 +8,6 @@ from functools import partial import logging from numbers import Number import statistics -from typing import Optional import voluptuous as vol @@ -17,6 +18,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, + SensorEntity, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -30,7 +32,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.util.decorator import Registry @@ -178,7 +179,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([SensorFilter(name, entity_id, filters)]) -class SensorFilter(Entity): +class SensorFilter(SensorEntity): """Representation of a Filter Sensor.""" def __init__(self, name, entity_id, filters): @@ -351,7 +352,7 @@ class SensorFilter(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ENTITY_ID: self._entity} @@ -394,8 +395,8 @@ class Filter: self, name, window_size: int = 1, - precision: Optional[int] = None, - entity: Optional[str] = None, + precision: int | None = None, + entity: str | None = None, ): """Initialize common attributes. @@ -453,7 +454,7 @@ class Filter: @FILTERS.register(FILTER_NAME_RANGE) -class RangeFilter(Filter): +class RangeFilter(Filter, SensorEntity): """Range filter. Determines if new state is in the range of upper_bound and lower_bound. @@ -463,9 +464,9 @@ class RangeFilter(Filter): def __init__( self, entity, - precision: Optional[int] = DEFAULT_PRECISION, - lower_bound: Optional[float] = None, - upper_bound: Optional[float] = None, + precision: int | None = DEFAULT_PRECISION, + lower_bound: float | None = None, + upper_bound: float | None = None, ): """Initialize Filter. @@ -508,7 +509,7 @@ class RangeFilter(Filter): @FILTERS.register(FILTER_NAME_OUTLIER) -class OutlierFilter(Filter): +class OutlierFilter(Filter, SensorEntity): """BASIC outlier filter. Determines if new state is in a band around the median. @@ -546,7 +547,7 @@ class OutlierFilter(Filter): @FILTERS.register(FILTER_NAME_LOWPASS) -class LowPassFilter(Filter): +class LowPassFilter(Filter, SensorEntity): """BASIC Low Pass Filter.""" def __init__(self, window_size, precision, entity, time_constant: int): @@ -570,7 +571,7 @@ class LowPassFilter(Filter): @FILTERS.register(FILTER_NAME_TIME_SMA) -class TimeSMAFilter(Filter): +class TimeSMAFilter(Filter, SensorEntity): """Simple Moving Average (SMA) Filter. The window_size is determined by time, and SMA is time weighted. @@ -616,7 +617,7 @@ class TimeSMAFilter(Filter): @FILTERS.register(FILTER_NAME_THROTTLE) -class ThrottleFilter(Filter): +class ThrottleFilter(Filter, SensorEntity): """Throttle Filter. One sample per window. @@ -639,7 +640,7 @@ class ThrottleFilter(Filter): @FILTERS.register(FILTER_NAME_TIME_THROTTLE) -class TimeThrottleFilter(Filter): +class TimeThrottleFilter(Filter, SensorEntity): """Time Throttle Filter. One sample per time period. diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 6cd62333a87..e7faff46155 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -8,10 +8,9 @@ from fints.client import FinTS3PinTanClient from fints.dialog import FinTSDialogError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -75,7 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in balance_accounts: if config[CONF_ACCOUNTS] and account.iban not in account_config: - _LOGGER.info("skipping account %s for bank %s", account.iban, fints_name) + _LOGGER.info("Skipping account %s for bank %s", account.iban, fints_name) continue account_name = account_config.get(account.iban) @@ -87,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in holdings_accounts: if config[CONF_HOLDINGS] and account.accountnumber not in holdings_config: _LOGGER.info( - "skipping holdings %s for bank %s", account.accountnumber, fints_name + "Skipping holdings %s for bank %s", account.accountnumber, fints_name ) continue @@ -154,7 +153,7 @@ class FinTsClient: return balance_accounts, holdings_accounts -class FinTsAccount(Entity): +class FinTsAccount(SensorEntity): """Sensor for a FinTS balance account. A balance account contains an amount of money (=balance). The amount may @@ -193,7 +192,7 @@ class FinTsAccount(Entity): return self._currency @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Additional attributes of the sensor.""" attributes = {ATTR_ACCOUNT: self._account.iban, ATTR_ACCOUNT_TYPE: "balance"} if self._client.name: @@ -206,7 +205,7 @@ class FinTsAccount(Entity): return ICON -class FinTsHoldingsAccount(Entity): +class FinTsHoldingsAccount(SensorEntity): """Sensor for a FinTS holdings account. A holdings account does not contain money but rather some financial @@ -238,7 +237,7 @@ class FinTsHoldingsAccount(Entity): return ICON @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Additional attributes of the sensor. Lists each holding of the account with the current value. diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index bf5f3f6beea..593109b4f52 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -26,13 +26,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -SUPPORTED_PLATFORMS = {SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN} - - -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the FireServiceRota component.""" - - return True +PLATFORMS = [SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -57,14 +51,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=MIN_TIME_BETWEEN_UPDATES, ) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: client, DATA_COORDINATOR: coordinator, } - for platform in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) @@ -83,7 +77,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in SUPPORTED_PLATFORMS + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index 3f04adc2f72..29fc97ae503 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -62,7 +62,7 @@ class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return available attributes for binary sensor.""" attr = {} if not self.coordinator.data: diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 83272eff926..04d8c97a4a5 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for FireServiceRota integration.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,7 +22,7 @@ async def async_setup_entry( async_add_entities([IncidentsSensor(client)]) -class IncidentsSensor(RestoreEntity): +class IncidentsSensor(RestoreEntity, SensorEntity): """Representation of FireServiceRota incidents sensor.""" def __init__(self, client): @@ -64,7 +65,7 @@ class IncidentsSensor(RestoreEntity): return False @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return available attributes for sensor.""" attr = {} data = self._state_attributes diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 7519270ca5c..e2385f02e5c 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -73,7 +73,7 @@ class ResponseSwitch(SwitchEntity): return self._client.on_duty @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return available attributes for switch.""" attr = {} if not self._state_attributes: diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json index 63c887ff281..8e8432d5df4 100644 --- a/homeassistant/components/fireservicerota/translations/hu.json +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { + "reauth": { + "data": { + "password": "Jelsz\u00f3" + } + }, "user": { "data": { - "url": "Weboldal" + "password": "Jelsz\u00f3", + "url": "Weboldal", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } diff --git a/homeassistant/components/fireservicerota/translations/id.json b/homeassistant/components/fireservicerota/translations/id.json new file mode 100644 index 00000000000..0c4462a1ea7 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "reauth": { + "data": { + "password": "Kata Sandi" + }, + "description": "Token autentikasi menjadi tidak valid, masuk untuk membuat token lagi." + }, + "user": { + "data": { + "password": "Kata Sandi", + "url": "Situs Web", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ko.json b/homeassistant/components/fireservicerota/translations/ko.json index f705fd9873c..843371ed035 100644 --- a/homeassistant/components/fireservicerota/translations/ko.json +++ b/homeassistant/components/fireservicerota/translations/ko.json @@ -14,11 +14,13 @@ "reauth": { "data": { "password": "\ube44\ubc00\ubc88\ud638" - } + }, + "description": "\uc778\uc99d \ud1a0\ud070\uc774 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc0dd\uc131\ud558\ub824\uba74 \ub85c\uadf8\uc778\ud574\uc8fc\uc138\uc694." }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", + "url": "\uc6f9\uc0ac\uc774\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json index 3955172e02d..046a65081ec 100644 --- a/homeassistant/components/fireservicerota/translations/ru.json +++ b/homeassistant/components/fireservicerota/translations/ru.json @@ -21,7 +21,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/firmata/config_flow.py b/homeassistant/components/firmata/config_flow.py index a86d97e9e2e..e9483998060 100644 --- a/homeassistant/components/firmata/config_flow.py +++ b/homeassistant/components/firmata/config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_NAME from .board import get_board -from .const import CONF_SERIAL_PORT, DOMAIN # pylint: disable=unused-import +from .const import CONF_SERIAL_PORT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 50ab58b9046..8f843d29272 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -1,5 +1,5 @@ """Entity for Firmata devices.""" -from typing import Type +from __future__ import annotations from homeassistant.config_entries import ConfigEntry @@ -32,7 +32,7 @@ class FirmataPinEntity(FirmataEntity): def __init__( self, - api: Type[FirmataBoardPin], + api: type[FirmataBoardPin], config_entry: ConfigEntry, name: str, pin: FirmataPinType, diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index e95b5101413..3c50a559e51 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -1,7 +1,7 @@ """Support for Firmata light output.""" +from __future__ import annotations import logging -from typing import Type from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -55,7 +55,7 @@ class FirmataLight(FirmataPinEntity, LightEntity): def __init__( self, - api: Type[FirmataBoardPin], + api: type[FirmataBoardPin], config_entry: ConfigEntry, name: str, pin: FirmataPinType, diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py index cb9db1f11e5..fedac6f76d9 100644 --- a/homeassistant/components/firmata/sensor.py +++ b/homeassistant/components/firmata/sensor.py @@ -2,10 +2,10 @@ import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE, DOMAIN from .entity import FirmataPinEntity @@ -42,7 +42,7 @@ async def async_setup_entry( async_add_entities(new_entities) -class FirmataSensor(FirmataPinEntity, Entity): +class FirmataSensor(FirmataPinEntity, SensorEntity): """Representation of a sensor on a Firmata board.""" async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/firmata/translations/fr.json b/homeassistant/components/firmata/translations/fr.json index a66d58dce87..b3509c9126c 100644 --- a/homeassistant/components/firmata/translations/fr.json +++ b/homeassistant/components/firmata/translations/fr.json @@ -2,6 +2,10 @@ "config": { "abort": { "cannot_connect": "Impossible de se connecter \u00e0 la carte Firmata pendant la configuration" + }, + "step": { + "one": "Vide ", + "other": "Vide" } } } \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/hu.json b/homeassistant/components/firmata/translations/hu.json new file mode 100644 index 00000000000..563ede56155 --- /dev/null +++ b/homeassistant/components/firmata/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/id.json b/homeassistant/components/firmata/translations/id.json new file mode 100644 index 00000000000..3f10b4aa77c --- /dev/null +++ b/homeassistant/components/firmata/translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 387eb78448c..8571d31bc8a 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -10,7 +10,7 @@ from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenEr import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_CLIENT_ID, @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.network import get_url from homeassistant.util.json import load_json, save_json @@ -403,7 +402,7 @@ class FitbitAuthCallbackView(HomeAssistantView): return html_response -class FitbitSensor(Entity): +class FitbitSensor(SensorEntity): """Implementation of a Fitbit sensor.""" def __init__( @@ -457,7 +456,7 @@ class FitbitSensor(Entity): return f"mdi:{FITBIT_RESOURCES_LIST[self.resource_type][2]}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {} diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 99ebbdd6bb6..9214dd6907e 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -6,10 +6,9 @@ from fixerio import Fixerio from fixerio.exceptions import FixerioException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TARGET import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ExchangeRateSensor(data, name, target)], True) -class ExchangeRateSensor(Entity): +class ExchangeRateSensor(SensorEntity): """Representation of a Exchange sensor.""" def __init__(self, data, name, target): @@ -75,7 +74,7 @@ class ExchangeRateSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.data.rate is not None: return { diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 450d09edeb8..cf4662b9866 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -1,6 +1,7 @@ """Platform for Flexit AC units with CI66 Modbus adapter.""" +from __future__ import annotations + import logging -from typing import List from pyflexit.pyflexit import pyflexit import voluptuous as vol @@ -93,7 +94,7 @@ class Flexit(ClimateEntity): self._current_operation = self.unit.get_operation @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return { "filter_hours": self._filter_hours, @@ -135,7 +136,7 @@ class Flexit(ClimateEntity): return self._current_operation @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index e81f8f2f5b0..6ddaddced0d 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -186,7 +186,7 @@ class FlicButton(BinarySensorEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return {"address": self.address} diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 2dba50cccca..6956c034d4a 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index 9d441ce7574..e6880600212 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -5,10 +5,10 @@ import logging import async_timeout from pyflick import FlickAPI, FlickPrice +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utcnow from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT, DOMAIN @@ -33,7 +33,7 @@ async def async_setup_entry( async_add_entities([FlickPricingSensor(api)], True) -class FlickPricingSensor(Entity): +class FlickPricingSensor(SensorEntity): """Entity object for Flick Electric sensor.""" def __init__(self, api: FlickAPI): @@ -61,7 +61,7 @@ class FlickPricingSensor(Entity): return UNIT_NAME @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index 3e3568c45f8..5cca1386ffb 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -12,6 +12,7 @@ "user": { "data": { "client_id": "Client-ID (optional)", + "client_secret": "Client Secret (optional)", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/flick_electric/translations/hu.json b/homeassistant/components/flick_electric/translations/hu.json index dee4ed9ee0f..f7ed726e433 100644 --- a/homeassistant/components/flick_electric/translations/hu.json +++ b/homeassistant/components/flick_electric/translations/hu.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k 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" + }, "step": { "user": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Flick Bejelentkez\u00e9si Adatok" } } } diff --git a/homeassistant/components/flick_electric/translations/id.json b/homeassistant/components/flick_electric/translations/id.json new file mode 100644 index 00000000000..8c283cfd56e --- /dev/null +++ b/homeassistant/components/flick_electric/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "client_id": "ID Klien (Opsional)", + "client_secret": "Kode Rahasia Klien (Opsional)", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Kredensial Masuk Flick" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ru.json b/homeassistant/components/flick_electric/translations/ru.json index bcabe2f2157..08bfc3ffb02 100644 --- a/homeassistant/components/flick_electric/translations/ru.json +++ b/homeassistant/components/flick_electric/translations/ru.json @@ -14,7 +14,7 @@ "client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Flick Electric" } diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index b57cdd5f871..71f8a8bfe5c 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -49,9 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): tasks = [device.async_refresh() for device in devices] await asyncio.gather(*tasks) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -62,8 +62,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index a8bac498674..bd623aa38bb 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -1,6 +1,5 @@ """Support for Flo Water Monitor binary sensors.""" - -from typing import List +from __future__ import annotations from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, @@ -14,10 +13,24 @@ from .entity import FloEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flo sensors from config entry.""" - devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ + devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ config_entry.entry_id ]["devices"] - entities = [FloPendingAlertsBinarySensor(device) for device in devices] + entities = [] + for device in devices: + if device.device_type == "puck_oem": + # Flo "pucks" (leak detectors) *do* support pending alerts. + # However these pending alerts mix unrelated issues like + # low-battery alerts, humidity alerts, & temperature alerts + # in addition to the critical "water detected" alert. + # + # Since there are non-binary sensors for battery, humidity, + # and temperature, the binary sensor should only cover + # water detection. If this sensor trips, you really have + # a problem - vs. battery/temp/humidity which are warnings. + entities.append(FloWaterDetectedBinarySensor(device)) + else: + entities.append(FloPendingAlertsBinarySensor(device)) async_add_entities(entities) @@ -34,7 +47,7 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): return self._device.has_alerts @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not self._device.has_alerts: return {} @@ -48,3 +61,21 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): def device_class(self): """Return the device class for the binary sensor.""" return DEVICE_CLASS_PROBLEM + + +class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): + """Binary sensor that reports if water is detected (for leak detectors).""" + + def __init__(self, device): + """Initialize the pending alerts binary sensor.""" + super().__init__("water_detected", "Water Detected", device) + + @property + def is_on(self): + """Return true if the Flo device is detecting water.""" + return self._device.water_detected + + @property + def device_class(self): + """Return the device class for the binary sensor.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py index 54c6ae94ee2..78899035bfa 100644 --- a/homeassistant/components/flo/config_flow.py +++ b/homeassistant/components/flo/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, LOGGER # pylint:disable=unused-import +from .const import DOMAIN, LOGGER DATA_SCHEMA = vol.Schema({"username": str, "password": str}) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index af36034026d..50e0ccda87f 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -1,7 +1,9 @@ """Flo device object.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta -from typing import Any, Dict, Optional +from typing import Any from aioflo.api import API from aioflo.errors import RequestError @@ -26,8 +28,8 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): self._flo_location_id: str = location_id self._flo_device_id: str = device_id self._manufacturer: str = "Flo by Moen" - self._device_information: Optional[Dict[str, Any]] = None - self._water_usage: Optional[Dict[str, Any]] = None + self._device_information: dict[str, Any] | None = None + self._water_usage: dict[str, Any] | None = None super().__init__( hass, LOGGER, @@ -58,7 +60,9 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): @property def device_name(self) -> str: """Return device name.""" - return f"{self.manufacturer} {self.model}" + return self._device_information.get( + "nickname", f"{self.manufacturer} {self.model}" + ) @property def manufacturer(self) -> str: @@ -120,6 +124,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Return the current temperature in degrees F.""" return self._device_information["telemetry"]["current"]["tempF"] + @property + def humidity(self) -> float: + """Return the current humidity in percent (0-100).""" + return self._device_information["telemetry"]["current"]["humidity"] + @property def consumption_today(self) -> float: """Return the current consumption for today in gallons.""" @@ -159,6 +168,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): or self.pending_warning_alerts_count ) + @property + def water_detected(self) -> bool: + """Return whether water is detected, for leak detectors.""" + return self._device_information["fwProperties"]["telemetry_water"] + @property def last_known_valve_state(self) -> str: """Return the last known valve state for the device.""" @@ -169,6 +183,11 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Return the target valve state for the device.""" return self._device_information["valve"]["target"] + @property + def battery_level(self) -> float: + """Return the battery level for battery-powered device, e.g. leak detectors.""" + return self._device_information["battery"]["level"] + async def async_set_mode_home(self): """Set the Flo location to home mode.""" await self.api_client.location.set_mode_home(self._flo_location_id) diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 35c6e022dcf..878c4188815 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -1,6 +1,7 @@ """Base entity class for Flo entities.""" +from __future__ import annotations -from typing import Any, Dict +from typing import Any from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import Entity @@ -36,7 +37,7 @@ class FloEntity(Entity): return self._unique_id @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return a device description for device registry.""" return { "identifiers": {(FLO_DOMAIN, self._device.id)}, diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 2feeb3702a6..1e362e75f8c 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -1,15 +1,17 @@ """Support for Flo Water Monitor sensors.""" +from __future__ import annotations -from typing import List, Optional - +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, PRESSURE_PSI, - TEMP_CELSIUS, + TEMP_FAHRENHEIT, VOLUME_GALLONS, ) -from homeassistant.util.temperature import fahrenheit_to_celsius from .const import DOMAIN as FLO_DOMAIN from .device import FloDeviceDataUpdateCoordinator @@ -20,25 +22,42 @@ GAUGE_ICON = "mdi:gauge" NAME_DAILY_USAGE = "Today's Water Usage" NAME_CURRENT_SYSTEM_MODE = "Current System Mode" NAME_FLOW_RATE = "Water Flow Rate" -NAME_TEMPERATURE = "Water Temperature" +NAME_WATER_TEMPERATURE = "Water Temperature" +NAME_AIR_TEMPERATURE = "Temperature" NAME_WATER_PRESSURE = "Water Pressure" +NAME_HUMIDITY = "Humidity" +NAME_BATTERY = "Battery" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flo sensors from config entry.""" - devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ + devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ config_entry.entry_id ]["devices"] entities = [] - entities.extend([FloDailyUsageSensor(device) for device in devices]) - entities.extend([FloSystemModeSensor(device) for device in devices]) - entities.extend([FloCurrentFlowRateSensor(device) for device in devices]) - entities.extend([FloTemperatureSensor(device) for device in devices]) - entities.extend([FloPressureSensor(device) for device in devices]) + for device in devices: + if device.device_type == "puck_oem": + entities.extend( + [ + FloTemperatureSensor(NAME_AIR_TEMPERATURE, device), + FloHumiditySensor(device), + FloBatterySensor(device), + ] + ) + else: + entities.extend( + [ + FloDailyUsageSensor(device), + FloSystemModeSensor(device), + FloCurrentFlowRateSensor(device), + FloTemperatureSensor(NAME_WATER_TEMPERATURE, device), + FloPressureSensor(device), + ] + ) async_add_entities(entities) -class FloDailyUsageSensor(FloEntity): +class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" def __init__(self, device): @@ -52,7 +71,7 @@ class FloDailyUsageSensor(FloEntity): return WATER_ICON @property - def state(self) -> Optional[float]: + def state(self) -> float | None: """Return the current daily usage.""" if self._device.consumption_today is None: return None @@ -64,7 +83,7 @@ class FloDailyUsageSensor(FloEntity): return VOLUME_GALLONS -class FloSystemModeSensor(FloEntity): +class FloSystemModeSensor(FloEntity, SensorEntity): """Monitors the current Flo system mode.""" def __init__(self, device): @@ -73,14 +92,14 @@ class FloSystemModeSensor(FloEntity): self._state: str = None @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the current system mode.""" if not self._device.current_system_mode: return None return self._device.current_system_mode -class FloCurrentFlowRateSensor(FloEntity): +class FloCurrentFlowRateSensor(FloEntity, SensorEntity): """Monitors the current water flow rate.""" def __init__(self, device): @@ -94,7 +113,7 @@ class FloCurrentFlowRateSensor(FloEntity): return GAUGE_ICON @property - def state(self) -> Optional[float]: + def state(self) -> float | None: """Return the current flow rate.""" if self._device.current_flow_rate is None: return None @@ -106,33 +125,59 @@ class FloCurrentFlowRateSensor(FloEntity): return "gpm" -class FloTemperatureSensor(FloEntity): +class FloTemperatureSensor(FloEntity, SensorEntity): """Monitors the temperature.""" - def __init__(self, device): + def __init__(self, name, device): """Initialize the temperature sensor.""" - super().__init__("temperature", NAME_TEMPERATURE, device) + super().__init__("temperature", name, device) self._state: float = None @property - def state(self) -> Optional[float]: + def state(self) -> float | None: """Return the current temperature.""" if self._device.temperature is None: return None - return round(fahrenheit_to_celsius(self._device.temperature), 1) + return round(self._device.temperature, 1) @property def unit_of_measurement(self) -> str: - """Return gallons as the unit measurement for water.""" - return TEMP_CELSIUS + """Return fahrenheit as the unit measurement for temperature.""" + return TEMP_FAHRENHEIT @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class for this sensor.""" return DEVICE_CLASS_TEMPERATURE -class FloPressureSensor(FloEntity): +class FloHumiditySensor(FloEntity, SensorEntity): + """Monitors the humidity.""" + + def __init__(self, device): + """Initialize the humidity sensor.""" + super().__init__("humidity", NAME_HUMIDITY, device) + self._state: float = None + + @property + def state(self) -> float | None: + """Return the current humidity.""" + if self._device.humidity is None: + return None + return round(self._device.humidity, 1) + + @property + def unit_of_measurement(self) -> str: + """Return percent as the unit measurement for humidity.""" + return PERCENTAGE + + @property + def device_class(self) -> str | None: + """Return the device class for this sensor.""" + return DEVICE_CLASS_HUMIDITY + + +class FloPressureSensor(FloEntity, SensorEntity): """Monitors the water pressure.""" def __init__(self, device): @@ -141,7 +186,7 @@ class FloPressureSensor(FloEntity): self._state: float = None @property - def state(self) -> Optional[float]: + def state(self) -> float | None: """Return the current water pressure.""" if self._device.current_psi is None: return None @@ -153,6 +198,30 @@ class FloPressureSensor(FloEntity): return PRESSURE_PSI @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class for this sensor.""" return DEVICE_CLASS_PRESSURE + + +class FloBatterySensor(FloEntity, SensorEntity): + """Monitors the battery level for battery-powered leak detectors.""" + + def __init__(self, device): + """Initialize the battery sensor.""" + super().__init__("battery", NAME_BATTERY, device) + self._state: float = None + + @property + def state(self) -> float | None: + """Return the current battery level.""" + return self._device.battery_level + + @property + def unit_of_measurement(self) -> str: + """Return percentage as the unit measurement for battery.""" + return PERCENTAGE + + @property + def device_class(self) -> str | None: + """Return the device class for this sensor.""" + return DEVICE_CLASS_BATTERY diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index 91f3fdf54e4..e5f00a6125f 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -1,6 +1,5 @@ """Switch representing the shutoff valve for the Flo by Moen integration.""" - -from typing import List +from __future__ import annotations from aioflo.location import SLEEP_MINUTE_OPTIONS, SYSTEM_MODE_HOME, SYSTEM_REVERT_MODES import voluptuous as vol @@ -23,10 +22,14 @@ SERVICE_RUN_HEALTH_TEST = "run_health_test" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flo switches from config entry.""" - devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ + devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ config_entry.entry_id ]["devices"] - async_add_entities([FloSwitch(device) for device in devices]) + entities = [] + for device in devices: + if device.device_type != "puck_oem": + entities.append(FloSwitch(device)) + async_add_entities(entities) platform = entity_platform.current_platform.get() diff --git a/homeassistant/components/flo/translations/hu.json b/homeassistant/components/flo/translations/hu.json index 3b2d79a34a7..0abcc301f0c 100644 --- a/homeassistant/components/flo/translations/hu.json +++ b/homeassistant/components/flo/translations/hu.json @@ -2,6 +2,20 @@ "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" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flo/translations/id.json b/homeassistant/components/flo/translations/id.json new file mode 100644 index 00000000000..ed8fde32106 --- /dev/null +++ b/homeassistant/components/flo/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", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/ru.json b/homeassistant/components/flo/translations/ru.json index 9e0db9fcf94..9b02cafd466 100644 --- a/homeassistant/components/flo/translations/ru.json +++ b/homeassistant/components/flo/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index b9a5fc17682..fb87588ac82 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -79,9 +79,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): FLUME_HTTP_SESSION: http_session, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -92,8 +92,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index e0e5bf8efe9..7a028b94eb0 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -14,8 +14,7 @@ from homeassistant.const import ( CONF_USERNAME, ) -from .const import BASE_TOKEN_FILENAME -from .const import DOMAIN # pylint:disable=unused-import +from .const import BASE_TOKEN_FILENAME, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 6b19c9c5476..d890443d238 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -6,7 +6,7 @@ from numbers import Number from pyflume import FlumeData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_CLIENT_ID, @@ -108,7 +108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(flume_entity_list) -class FlumeSensor(CoordinatorEntity): +class FlumeSensor(CoordinatorEntity, SensorEntity): """Representation of the Flume sensor.""" def __init__(self, coordinator, flume_device, flume_query_sensor, name, device_id): @@ -175,7 +175,7 @@ def _create_flume_device_coordinator(hass, flume_device): _LOGGER.debug("Updating Flume data") try: await hass.async_add_executor_job(flume_device.update_force) - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: raise UpdateFailed(f"Error communicating with flume API: {ex}") from ex _LOGGER.debug( "Flume update details: %s", diff --git a/homeassistant/components/flume/translations/he.json b/homeassistant/components/flume/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/flume/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json index dee4ed9ee0f..cc0c820facf 100644 --- a/homeassistant/components/flume/translations/hu.json +++ b/homeassistant/components/flume/translations/hu.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k 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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flume/translations/id.json b/homeassistant/components/flume/translations/id.json new file mode 100644 index 00000000000..333afb167e6 --- /dev/null +++ b/homeassistant/components/flume/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "client_id": "ID Klien", + "client_secret": "Kode Rahasia Klien", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Untuk mengakses API Flume Personal, Anda harus meminta 'ID Klien' dan 'Kode Rahasia Klien' di https://portal.flumetech.com/settings#token", + "title": "Hubungkan ke Akun Flume Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/ko.json b/homeassistant/components/flume/translations/ko.json index b700854ab57..c82f0a990d8 100644 --- a/homeassistant/components/flume/translations/ko.json +++ b/homeassistant/components/flume/translations/ko.json @@ -16,7 +16,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "Flume Personal API \uc5d0 \uc561\uc138\uc2a4 \ud558\ub824\uba74 https://portal.flumetech.com/settings#token \uc5d0\uc11c '\ud074\ub77c\uc774\uc5b8\ud2b8 ID'\ubc0f '\ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf'\uc744 \uc694\uccad\ud574\uc57c \ud569\ub2c8\ub2e4.", + "description": "Flume Personal API \uc5d0 \uc811\uadfc\ud558\ub824\uba74 https://portal.flumetech.com/settings#token \uc5d0\uc11c '\ud074\ub77c\uc774\uc5b8\ud2b8 ID'\ubc0f '\ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf'\uc744 \uc694\uccad\ud574\uc57c \ud569\ub2c8\ub2e4.", "title": "Flume \uacc4\uc815\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/flume/translations/nl.json b/homeassistant/components/flume/translations/nl.json index d176eb13365..97daf42d11e 100644 --- a/homeassistant/components/flume/translations/nl.json +++ b/homeassistant/components/flume/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dit account is al geconfigureerd." + "already_configured": "Account is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/flume/translations/ru.json b/homeassistant/components/flume/translations/ru.json index e4be913abcd..757ec6e5226 100644 --- a/homeassistant/components/flume/translations/ru.json +++ b/homeassistant/components/flume/translations/ru.json @@ -14,7 +14,7 @@ "client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430", "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u043c\u0443 API Flume, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 'ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430' \u0438 '\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430' \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://portal.flumetech.com/settings#token.", "title": "Flume" diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 46442f112b6..8e5e3762f32 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -67,9 +67,9 @@ async def async_setup_entry(hass, config_entry): await asyncio.gather(*data_init_tasks) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -80,8 +80,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py index 8bdef58f7e1..5229394b58b 100644 --- a/homeassistant/components/flunearyou/config_flow.py +++ b/homeassistant/components/flunearyou/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN, LOGGER # pylint: disable=unused-import +from .const import DOMAIN, LOGGER class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 8bb5f1317d1..066126c390e 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -1,4 +1,5 @@ """Support for user- and CDC-based flu info sensors from Flu Near You.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_STATE, @@ -85,7 +86,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class FluNearYouSensor(CoordinatorEntity): +class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" def __init__(self, coordinator, config_entry, sensor_type, name, icon, unit): @@ -100,7 +101,7 @@ class FluNearYouSensor(CoordinatorEntity): self._unit = unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/flunearyou/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json new file mode 100644 index 00000000000..4f8cca2a939 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/id.json b/homeassistant/components/flunearyou/translations/id.json new file mode 100644 index 00000000000..86afc7bb5fd --- /dev/null +++ b/homeassistant/components/flunearyou/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur" + }, + "description": "Pantau laporan berbasis pengguna dan CDC berdasarkan data koordinat.", + "title": "Konfigurasikan Flu Near You" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ko.json b/homeassistant/components/flunearyou/translations/ko.json index f6528e85cca..bfe1945fa67 100644 --- a/homeassistant/components/flunearyou/translations/ko.json +++ b/homeassistant/components/flunearyou/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json index 0ff044abc5e..d78abfcc187 100644 --- a/homeassistant/components/flunearyou/translations/nl.json +++ b/homeassistant/components/flunearyou/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd." + "already_configured": "Locatie is al geconfigureerd." }, "error": { "unknown": "Onverwachte fout" @@ -12,7 +12,7 @@ "latitude": "Breedtegraad", "longitude": "Lengtegraad" }, - "description": "Bewaak op gebruikers gebaseerde en CDC-repots voor een paar co\u00f6rdinaten.", + "description": "Bewaak gebruikersgebaseerde en CDC-rapporten voor een paar co\u00f6rdinaten.", "title": "Configureer \nFlu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/zh-Hans.json b/homeassistant/components/flunearyou/translations/zh-Hans.json new file mode 100644 index 00000000000..f55159dc235 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u4f4d\u7f6e\u5b8c\u6210\u914d\u7f6e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 4bfd0c0a26c..2f8d2cc5536 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -98,7 +98,7 @@ TRANSITION_GRADUAL = "gradual" TRANSITION_JUMP = "jump" TRANSITION_STROBE = "strobe" -FLUX_EFFECT_LIST = sorted(list(EFFECT_MAP)) + [EFFECT_RANDOM] +FLUX_EFFECT_LIST = sorted(EFFECT_MAP) + [EFFECT_RANDOM] CUSTOM_EFFECT_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index 9a062133718..707f22f98ba 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -6,10 +6,9 @@ import os import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -45,13 +44,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): path = config.get(CONF_FOLDER_PATHS) if not hass.config.is_allowed_path(path): - _LOGGER.error("folder %s is not valid or allowed", path) + _LOGGER.error("Folder %s is not valid or allowed", path) else: folder = Folder(path, config.get(CONF_FILTER)) add_entities([folder], True) -class Folder(Entity): +class Folder(SensorEntity): """Representation of a folder.""" ICON = "mdi:folder" @@ -92,7 +91,7 @@ class Folder(Entity): return self.ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" return { "path": self._folder_path, diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index d99e4928cc5..7d3fd5e77a7 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -43,7 +43,7 @@ def setup(hass, config): path = watcher[CONF_FOLDER] patterns = watcher[CONF_PATTERNS] if not hass.config.is_allowed_path(path): - _LOGGER.error("folder %s is not valid or allowed", path) + _LOGGER.error("Folder %s is not valid or allowed", path) return False Watcher(path, patterns, hass) diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 9ca265ba9ef..996ac1b1049 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -7,6 +7,7 @@ import aiohttp from foobot_async import FoobotClient import voluptuous as vol +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_TIME, @@ -91,7 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class FoobotSensor(Entity): +class FoobotSensor(SensorEntity): """Implementation of a Foobot sensor.""" def __init__(self, data, device, sensor_type): diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 45cefc739b9..0186b18ee74 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -4,11 +4,6 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from .const import DOMAIN, HASS_DATA_REMOVE_LISTENERS_KEY, HASS_DATA_UPDATER_KEY -async def async_setup(hass, config): - """Set up the forked-daapd component.""" - return True - - async def async_setup_entry(hass, entry): """Set up forked-daapd from a config entry by forwarding to platform.""" hass.async_create_task( diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index adc6e9b7b35..6bcc35f0a52 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure forked-daapd devices.""" +from contextlib import suppress import logging from pyforked_daapd import ForkedDaapdAPI @@ -9,7 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( # pylint:disable=unused-import +from .const import ( CONF_LIBRESPOT_JAVA_PORT, CONF_MAX_PLAYLISTS, CONF_TTS_PAUSE_TIME, @@ -161,12 +162,10 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if discovery_info.get("properties") and discovery_info["properties"].get( "Machine Name" ): - try: + with suppress(ValueError): version_num = int( discovery_info["properties"].get("mtd-version", "0").split(".")[0] ) - except ValueError: - pass if version_num < 27: return self.async_abort(reason="not_forked_daapd") await self.async_set_unique_id(discovery_info["properties"]["Machine Name"]) diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index 15f043dbbfe..b9f78875a2d 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -1,7 +1,7 @@ { "domain": "forked_daapd", "name": "forked-daapd", - "documentation": "https://www.home-assistant.io/integrations/forked-daapd", + "documentation": "https://www.home-assistant.io/integrations/forked_daapd", "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], "config_flow": true, diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index e90ffc71f90..fcbcf6b0df0 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "forbidden": "Verbindung kann nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Ihre forked-daapd-Netzwerkberechtigungen.", "unknown_error": "Unbekannter Fehler", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index a9c13f1ee68..ca90fad3048 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket." + "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/id.json b/homeassistant/components/forked_daapd/translations/id.json new file mode 100644 index 00000000000..76787e2a19b --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "not_forked_daapd": "Perangkat bukan server forked-daapd." + }, + "error": { + "forbidden": "Tidak dapat terhubung. Periksa izin jaringan forked-daapd Anda.", + "unknown_error": "Kesalahan yang tidak diharapkan", + "websocket_not_enabled": "Websocket server forked-daapd tidak diaktifkan.", + "wrong_host_or_port": "Tidak dapat terhubung. Periksa nilai host dan port.", + "wrong_password": "Kata sandi salah.", + "wrong_server_type": "Integrasi forked-daapd membutuhkan server forked-daapd dengan versi >= 27.0." + }, + "flow_title": "forked-daapd server: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama alias", + "password": "Kata sandi API (kosongkan jika tidak ada kata sandi)", + "port": "Port API" + }, + "title": "Siapkan perangkat forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port untuk kontrol pipa librespot-java (jika digunakan)", + "max_playlists": "Jumlah maksimum daftar putar yang digunakan sebagai sumber", + "tts_pause_time": "Tenggang waktu dalam detik sebelum dan setelah TTS", + "tts_volume": "Volume TTS (bilangan float dalam rentang [0,1])" + }, + "description": "Tentukan berbagai opsi untuk integrasi forked-daapd.", + "title": "Konfigurasikan opsi forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ko.json b/homeassistant/components/forked_daapd/translations/ko.json index 5ae487a4096..60b585af958 100644 --- a/homeassistant/components/forked_daapd/translations/ko.json +++ b/homeassistant/components/forked_daapd/translations/ko.json @@ -5,6 +5,7 @@ "not_forked_daapd": "\uae30\uae30\uac00 forked-daapd \uc11c\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4." }, "error": { + "forbidden": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. fork-daapd \ub124\ud2b8\uc6cc\ud06c \uc0ac\uc6a9 \uad8c\ud55c\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694", "unknown_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "websocket_not_enabled": "forked-daapd \uc11c\ubc84 \uc6f9\uc18c\ucf13\uc774 \ube44\ud65c\uc131\ud654 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.", "wrong_host_or_port": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", @@ -30,11 +31,11 @@ "data": { "librespot_java_port": "librespot-java \ud30c\uc774\ud504 \ucee8\ud2b8\ub864\uc6a9 \ud3ec\ud2b8 (\uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0)", "max_playlists": "\uc18c\uc2a4\ub85c \uc0ac\uc6a9\ub41c \ucd5c\ub300 \uc7ac\uc0dd \ubaa9\ub85d \uc218", - "tts_pause_time": "TTS \uc804\ud6c4\uc5d0 \uc77c\uc2dc\uc911\uc9c0\ud560 \uc2dc\uac04(\ucd08)", + "tts_pause_time": "TTS \uc804\ud6c4\uc5d0 \uc77c\uc2dc\uc911\uc9c0\ud560 \uc2dc\uac04 (\ucd08)", "tts_volume": "TTS \ubcfc\ub968 (0~1 \uc758 \uc2e4\uc218\uac12)" }, "description": "forked-daapd \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ub2e4\uc591\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "forked-daapd \uc635\uc158 \uc124\uc815\ud558\uae30" + "title": "forked-daapd \uc635\uc158 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/forked_daapd/translations/nl.json b/homeassistant/components/forked_daapd/translations/nl.json index 5391615fcb1..7eec6a34571 100644 --- a/homeassistant/components/forked_daapd/translations/nl.json +++ b/homeassistant/components/forked_daapd/translations/nl.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "not_forked_daapd": "Apparaat is geen forked-daapd-server." }, "error": { - "unknown_error": "Onbekende fout.", + "forbidden": "Niet in staat te verbinden. Controleer alstublieft uw forked-daapd netwerkrechten.", + "unknown_error": "Onverwachte fout", "websocket_not_enabled": "forked-daapd server websocket niet ingeschakeld.", "wrong_host_or_port": "Verbinding mislukt, controleer het host-adres en poort.", "wrong_password": "Onjuist wachtwoord.", diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 6a2c961544f..1b3ae5e7216 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -22,9 +22,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up foscam from a config entry.""" - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) hass.data[DOMAIN][entry.entry_id] = entry.data @@ -37,8 +37,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index d600546c3b0..ea20f0a07fb 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -68,7 +68,7 @@ PTZ_GOTO_PRESET_COMMAND = "ptz_goto_preset" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" LOGGER.warning( - "Loading foscam via platform config is deprecated, it will be automatically imported. Please remove it afterwards." + "Loading foscam via platform config is deprecated, it will be automatically imported; Please remove it afterwards" ) config_new = { diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index bfeefb9e406..5ec36c97fa0 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -17,8 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import AbortFlow -from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_RTSP_PORT, CONF_STREAM, DOMAIN, LOGGER STREAMS = ["Main", "Sub"] @@ -128,16 +127,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._validate_and_create(import_config) except CannotConnect: - LOGGER.error("Error importing foscam platform config: cannot connect.") + LOGGER.error("Error importing foscam platform config: cannot connect") return self.async_abort(reason="cannot_connect") except InvalidAuth: - LOGGER.error("Error importing foscam platform config: invalid auth.") + LOGGER.error("Error importing foscam platform config: invalid auth") return self.async_abort(reason="invalid_auth") except InvalidResponse: LOGGER.exception( - "Error importing foscam platform config: invalid response from camera." + "Error importing foscam platform config: invalid response from camera" ) return self.async_abort(reason="invalid_response") @@ -146,7 +145,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except LOGGER.exception( - "Error importing foscam platform config: unexpected exception." + "Error importing foscam platform config: unexpected exception" ) return self.async_abort(reason="unknown") diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json new file mode 100644 index 00000000000..5c41e03c838 --- /dev/null +++ b/homeassistant/components/foscam/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "rtsp_port": "RTSP \u043f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json index 603be1847cc..d87044b579a 100644 --- a/homeassistant/components/foscam/translations/de.json +++ b/homeassistant/components/foscam/translations/de.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_response": "Ung\u00fcltige Antwort vom Ger\u00e4t", "unknown": "Unerwarteter Fehler" }, "step": { @@ -14,6 +15,7 @@ "host": "Host", "password": "Passwort", "port": "Port", + "rtsp_port": "RTSP-Port", "username": "Benutzername" } } diff --git a/homeassistant/components/foscam/translations/hu.json b/homeassistant/components/foscam/translations/hu.json new file mode 100644 index 00000000000..63ea95210ff --- /dev/null +++ b/homeassistant/components/foscam/translations/hu.json @@ -0,0 +1,26 @@ +{ + "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", + "invalid_response": "\u00c9rv\u00e9nytelen v\u00e1lasz az eszk\u00f6zt\u0151l", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "rtsp_port": "RTSP port", + "stream": "Stream", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/id.json b/homeassistant/components/foscam/translations/id.json new file mode 100644 index 00000000000..21a7682b92c --- /dev/null +++ b/homeassistant/components/foscam/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_response": "Response tidak valid dari perangkat", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "rtsp_port": "Port RTSP", + "stream": "Stream", + "username": "Nama Pengguna" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ko.json b/homeassistant/components/foscam/translations/ko.json index bfd8e952671..ba743f6b27a 100644 --- a/homeassistant/components/foscam/translations/ko.json +++ b/homeassistant/components/foscam/translations/ko.json @@ -6,6 +6,7 @@ "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_response": "\uae30\uae30\uc5d0\uc11c \uc798\ubabb\ub41c \uc751\ub2f5\uc744 \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { @@ -14,9 +15,12 @@ "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", + "rtsp_port": "RTSP \ud3ec\ud2b8", + "stream": "\uc2a4\ud2b8\ub9bc", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } - } + }, + "title": "Foscam" } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json index f78f64af69a..8e8404c501e 100644 --- a/homeassistant/components/foscam/translations/ru.json +++ b/homeassistant/components/foscam/translations/ru.json @@ -17,7 +17,7 @@ "port": "\u041f\u043e\u0440\u0442", "rtsp_port": "\u041f\u043e\u0440\u0442 RTSP", "stream": "\u041f\u043e\u0442\u043e\u043a", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 9120c7d0866..c6c98e6c2df 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -4,13 +4,12 @@ import logging import voluptuous as vol -from homeassistant.components.discovery import SERVICE_FREEBOX -from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -26,41 +25,20 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): - """Set up the Freebox component.""" - conf = config.get(DOMAIN) - - async def discovery_dispatch(service, discovery_info): - if conf is None: - host = discovery_info.get("properties", {}).get("api_domain") - port = discovery_info.get("properties", {}).get("https_port") - _LOGGER.info("Discovered Freebox server: %s:%s", host, port) + """Set up the Freebox integration.""" + if DOMAIN in config: + for entry_config in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_DISCOVERY}, - data={CONF_HOST: host, CONF_PORT: port}, + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config ) ) - discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch) - - if conf is None: - return True - - for freebox_conf in conf: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=freebox_conf, - ) - ) - return True async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): - """Set up Freebox component.""" + """Set up Freebox entry.""" router = FreeboxRouter(hass, entry) await router.setup() @@ -77,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Handle reboot service call.""" await router.reboot() - hass.services.async_register(DOMAIN, "reboot", async_reboot) + hass.services.async_register(DOMAIN, SERVICE_REBOOT, async_reboot) async def async_close_connection(event): """Close Freebox connection on HA Stop.""" @@ -101,5 +79,6 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): if unload_ok: router = hass.data[DOMAIN].pop(entry.unique_id) await router.close() + hass.services.async_remove(DOMAIN, SERVICE_REBOOT) return unload_ok diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 2ee52884c88..f2e115ddf9b 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN from .router import get_api _LOGGER = logging.getLogger(__name__) @@ -106,6 +106,8 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_discovery(self, discovery_info): - """Initialize step from discovery.""" - return await self.async_step_user(discovery_info) + async def async_step_zeroconf(self, discovery_info: dict): + """Initialize flow from zeroconf.""" + host = discovery_info["properties"]["api_domain"] + port = discovery_info["properties"]["https_port"] + return await self.async_step_user({CONF_HOST: host, CONF_PORT: port}) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index d0ac63fa9bb..df251dcf954 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -4,10 +4,12 @@ import socket from homeassistant.const import ( DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, ) DOMAIN = "freebox" +SERVICE_REBOOT = "reboot" APP_DESC = { "app_id": "hass", @@ -55,6 +57,15 @@ CALL_SENSORS = { }, } +DISK_PARTITION_SENSORS = { + "partition_free_space": { + SENSOR_NAME: "free space", + SENSOR_UNIT: PERCENTAGE, + SENSOR_ICON: "mdi:harddisk", + SENSOR_DEVICE_CLASS: None, + }, +} + TEMPERATURE_SENSOR_TEMPLATE = { SENSOR_NAME: None, SENSOR_UNIT: TEMP_CELSIUS, diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 10c5b8eb2c5..6510a29bbfc 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,6 +1,7 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from __future__ import annotations + from datetime import datetime -from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -52,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 @@ -105,12 +106,12 @@ class FreeboxDevice(ScannerEntity): return self._icon @property - def device_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 2739849b547..2d55553511b 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -3,7 +3,7 @@ "name": "Freebox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/freebox", - "requirements": ["freebox-api==0.0.9"], - "after_dependencies": ["discovery"], + "requirements": ["freebox-api==0.0.10"], + "zeroconf": ["_fbx-api._tcp.local."], "codeowners": ["@hacf-fr", "@Quentame"] } diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 2511280f719..fbeca869d1d 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -1,8 +1,11 @@ """Represent the Freebox router and its devices and sensors.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging +import os from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any from freebox_api import Freepybox from freebox_api.api.wifi import Wifi @@ -31,6 +34,18 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) +async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: + """Get the Freebox API.""" + freebox_path = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path + + if not os.path.exists(freebox_path): + await hass.async_add_executor_job(os.makedirs, freebox_path) + + token_file = Path(f"{freebox_path}/{slugify(host)}.conf") + + return Freepybox(APP_DESC, token_file, API_VERSION) + + class FreeboxRouter: """Representation of a Freebox router.""" @@ -42,15 +57,16 @@ class FreeboxRouter: self._port = entry.data[CONF_PORT] self._api: Freepybox = None - self._name = None + self.name = None self.mac = None self._sw_v = None self._attrs = {} - self.devices: Dict[str, Any] = {} - self.sensors_temperature: Dict[str, int] = {} - self.sensors_connection: Dict[str, float] = {} - self.call_list: List[Dict[str, Any]] = [] + self.devices: dict[str, dict[str, Any]] = {} + self.disks: dict[int, dict[str, Any]] = {} + self.sensors_temperature: dict[str, int] = {} + self.sensors_connection: dict[str, float] = {} + self.call_list: list[dict[str, Any]] = [] self._unsub_dispatcher = None self.listeners = [] @@ -68,7 +84,7 @@ class FreeboxRouter: # System fbx_config = await self._api.system.get_config() self.mac = fbx_config["mac"] - self._name = fbx_config["model_info"]["pretty_name"] + self.name = fbx_config["model_info"]["pretty_name"] self._sw_v = fbx_config["firmware_version"] # Devices & sensors @@ -77,20 +93,20 @@ class FreeboxRouter: self.hass, self.update_all, SCAN_INTERVAL ) - async def update_all(self, now: Optional[datetime] = None) -> None: + async def update_all(self, now: datetime | None = None) -> None: """Update all Freebox platforms.""" + await self.update_device_trackers() await self.update_sensors() - await self.update_devices() - async def update_devices(self) -> None: + async def update_device_trackers(self) -> None: """Update Freebox devices.""" new_device = False - fbx_devices: Dict[str, Any] = await self._api.lan.get_hosts_list() + fbx_devices: [dict[str, Any]] = await self._api.lan.get_hosts_list() # Adds the Freebox itself fbx_devices.append( { - "primary_name": self._name, + "primary_name": self.name, "l2ident": {"id": self.mac}, "vendor_name": "Freebox SAS", "host_type": "router", @@ -115,7 +131,7 @@ class FreeboxRouter: async def update_sensors(self) -> None: """Update Freebox sensors.""" # System sensors - syst_datas: Dict[str, Any] = await self._api.system.get_config() + syst_datas: dict[str, Any] = await self._api.system.get_config() # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. @@ -123,7 +139,7 @@ class FreeboxRouter: self.sensors_temperature[sensor["name"]] = sensor["value"] # Connection sensors - connection_datas: Dict[str, Any] = await self._api.connection.get_status() + connection_datas: dict[str, Any] = await self._api.connection.get_status() for sensor_key in CONNECTION_SENSORS: self.sensors_connection[sensor_key] = connection_datas[sensor_key] @@ -140,8 +156,18 @@ class FreeboxRouter: self.call_list = await self._api.call.get_calls_log() + await self._update_disks_sensors() + async_dispatcher_send(self.hass, self.signal_sensor_update) + async def _update_disks_sensors(self) -> None: + """Update Freebox disks.""" + # None at first request + fbx_disks: [dict[str, Any]] = await self._api.storage.get_disks() or [] + + for fbx_disk in fbx_disks: + self.disks[fbx_disk["id"]] = fbx_disk + async def reboot(self) -> None: """Reboot the Freebox.""" await self._api.system.reboot() @@ -154,12 +180,12 @@ class FreeboxRouter: self._api = None @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)}, "identifiers": {(DOMAIN, self.mac)}, - "name": self._name, + "name": self.name, "manufacturer": "Freebox SAS", "sw_version": self._sw_v, } @@ -180,7 +206,7 @@ class FreeboxRouter: return f"{DOMAIN}-{self._host}-sensor-update" @property - def sensors(self) -> Dict[str, Any]: + def sensors(self) -> dict[str, Any]: """Return sensors.""" return {**self.sensors_temperature, **self.sensors_connection} @@ -188,13 +214,3 @@ class FreeboxRouter: def wifi(self) -> Wifi: """Return the wifi.""" return self._api.wifi - - -async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: - """Get the Freebox API.""" - freebox_path = Path(hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path) - freebox_path.mkdir(exist_ok=True) - - token_file = Path(f"{freebox_path}/{slugify(host)}.conf") - - return Freepybox(APP_DESC, token_file, API_VERSION) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index aeeaba438ff..fd0685f7667 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,17 +1,20 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -from typing import Dict +from __future__ import annotations +import logging + +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.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from .const import ( CALL_SENSORS, CONNECTION_SENSORS, + DISK_PARTITION_SENSORS, DOMAIN, SENSOR_DEVICE_CLASS, SENSOR_ICON, @@ -21,6 +24,8 @@ from .const import ( ) from .router import FreeboxRouter +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -29,6 +34,12 @@ async def async_setup_entry( router = hass.data[DOMAIN][entry.unique_id] entities = [] + _LOGGER.debug( + "%s - %s - %s temperature sensors", + router.name, + router.mac, + len(router.sensors_temperature), + ) for sensor_name in router.sensors_temperature: entities.append( FreeboxSensor( @@ -46,14 +57,28 @@ async def async_setup_entry( for sensor_key in CALL_SENSORS: entities.append(FreeboxCallSensor(router, sensor_key, CALL_SENSORS[sensor_key])) + _LOGGER.debug("%s - %s - %s disk(s)", router.name, router.mac, len(router.disks)) + for disk in router.disks.values(): + for partition in disk["partitions"]: + for sensor_key in DISK_PARTITION_SENSORS: + entities.append( + FreeboxDiskSensor( + router, + disk, + partition, + sensor_key, + DISK_PARTITION_SENSORS[sensor_key], + ) + ) + async_add_entities(entities, True) -class FreeboxSensor(Entity): +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 @@ -105,7 +130,7 @@ class FreeboxSensor(Entity): 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 @@ -136,11 +161,11 @@ 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.""" - self._call_list_for_type = [] super().__init__(router, sensor_type, sensor) + self._call_list_for_type = [] @callback def async_update_state(self) -> None: @@ -156,9 +181,49 @@ class FreeboxCallSensor(FreeboxSensor): self._state = len(self._call_list_for_type) @property - def device_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"] for call in self._call_list_for_type } + + +class FreeboxDiskSensor(FreeboxSensor): + """Representation of a Freebox disk sensor.""" + + def __init__( + self, + router: FreeboxRouter, + disk: dict[str, any], + partition: dict[str, any], + sensor_type: str, + sensor: dict[str, any], + ) -> None: + """Initialize a Freebox disk sensor.""" + super().__init__(router, sensor_type, sensor) + self._disk = disk + self._partition = partition + self._name = f"{partition['label']} {sensor[SENSOR_NAME]}" + self._unique_id = f"{self._router.mac} {sensor_type} {self._disk['id']} {self._partition['id']}" + + @property + def device_info(self) -> dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._disk["id"])}, + "name": f"Disk {self._disk['id']}", + "model": self._disk["model"], + "sw_version": self._disk["firmware"], + "via_device": ( + DOMAIN, + self._router.mac, + ), + } + + @callback + def async_update_state(self) -> None: + """Update the Freebox disk sensor.""" + self._state = round( + self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 + ) diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index b1cfc93eb53..a15a86f46d8 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -1,6 +1,7 @@ """Support for Freebox Delta, Revolution and Mini 4K.""" +from __future__ import annotations + import logging -from typing import Dict from freebox_api.exceptions import InsufficientPermissionsError @@ -48,7 +49,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/hu.json b/homeassistant/components/freebox/translations/hu.json index d13f5fa17c8..1f0b848d3b6 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/freebox/translations/id.json b/homeassistant/components/freebox/translations/id.json new file mode 100644 index 00000000000..b03ec248edb --- /dev/null +++ b/homeassistant/components/freebox/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "register_failed": "Gagal mendaftar, coba lagi.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "link": { + "description": "Klik \"Kirim\", lalu sentuh panah kanan di router untuk mendaftarkan Freebox dengan Home Assistant. \n\n![Lokasi tombol di router](/static/images/config_freebox.png)", + "title": "Tautkan router Freebox" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json index a8b9a1edc7a..c3bb5a9bd40 100644 --- a/homeassistant/components/freebox/translations/ko.json +++ b/homeassistant/components/freebox/translations/ko.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant \uc5d0 Freebox \ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n![\ub77c\uc6b0\ud130\uc758 \ubc84\ud2bc \uc704\uce58](/static/images/config_freebox.png)", + "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant\uc5d0 Freebox\ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n![\ub77c\uc6b0\ud130\uc758 \ubc84\ud2bc \uc704\uce58](/static/images/config_freebox.png)", "title": "Freebox \ub77c\uc6b0\ud130 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/freebox/translations/nl.json b/homeassistant/components/freebox/translations/nl.json index ea41fcfcd6a..7fbd57dd6ff 100644 --- a/homeassistant/components/freebox/translations/nl.json +++ b/homeassistant/components/freebox/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Host is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "register_failed": "Registratie is mislukt, probeer het opnieuw", - "unknown": "Onbekende fout: probeer het later nog eens" + "unknown": "Onverwachte fout" }, "step": { "link": { diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 3c01657da4e..34f56ddc6f9 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -93,9 +93,9 @@ async def async_setup_entry(hass, entry): hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) def logout_fritzbox(event): @@ -115,8 +115,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 4abe82776a9..50f56f3d510 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -191,7 +191,7 @@ class FritzboxThermostat(ClimateEntity): return MAX_TEMPERATURE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attrs = { ATTR_STATE_BATTERY_LOW: self._device.battery_low, diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 904081ef99f..a462f885484 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.components.ssdp import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -# pylint:disable=unused-import from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN DATA_SCHEMA_USER = vol.Schema( diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 85238d80f27..4d9c1693c1f 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,8 +1,8 @@ """Support for AVM Fritz!Box smarthome temperature sensor only devices.""" import requests +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS -from homeassistant.helpers.entity import Entity from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class FritzBoxTempSensor(Entity): +class FritzBoxTempSensor(SensorEntity): """The entity class for Fritzbox temperature sensors.""" def __init__(self, device, fritz): @@ -80,7 +80,7 @@ class FritzBoxTempSensor(Entity): self._fritz.login() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" attrs = { ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index b179464182f..50c60f7bb39 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -93,7 +93,7 @@ class FritzboxSwitch(SwitchEntity): self._fritz.login() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" attrs = {} attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock diff --git a/homeassistant/components/fritzbox/translations/bg.json b/homeassistant/components/fritzbox/translations/bg.json new file mode 100644 index 00000000000..ec678d2d76c --- /dev/null +++ b/homeassistant/components/fritzbox/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json new file mode 100644 index 00000000000..035cb07a170 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 6a08b68d863..44b68d5f540 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "flow_title": "AVM FRITZ!Box: {name}", "step": { "confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + }, + "reauth_confirm": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/fritzbox/translations/id.json b/homeassistant/components/fritzbox/translations/id.json new file mode 100644 index 00000000000..8dbd1f71534 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/id.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Tersambung ke AVM FRITZ!Box tetapi tidak dapat mengontrol perangkat Smart Home.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Ingin menyiapkan {name}?" + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Perbarui informasi masuk Anda untuk {name}." + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan informasi AVM FRITZ!Box Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ko.json b/homeassistant/components/fritzbox/translations/ko.json index dfdcc0ad4eb..21fd6b6afdf 100644 --- a/homeassistant/components/fritzbox/translations/ko.json +++ b/homeassistant/components/fritzbox/translations/ko.json @@ -4,7 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "not_supported": "AVM FRITZ!Box \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "not_supported": "AVM FRITZ!Box\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc2a4\ub9c8\ud2b8 \ud648 \uae30\uae30\ub97c \uc81c\uc5b4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -17,13 +17,14 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "reauth_confirm": { "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "description": "{name}\uc5d0 \ub300\ud55c \ub85c\uadf8\uc778 \uc815\ubcf4\ub97c \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index 9bfe2ef6be6..aa4f796f44c 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Deze AVM FRITZ!Box is al geconfigureerd.", - "already_in_progress": "AVM FRITZ!Box configuratie is al bezig.", + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen.", "reauth_successful": "Herauthenticatie was succesvol" @@ -23,11 +23,12 @@ "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "description": "Gelieve uw logingegevens voor {name} te actualiseren." }, "user": { "data": { - "host": "Host of IP-adres", + "host": "Host", "password": "Wachtwoord", "username": "Gebruikersnaam" }, diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 8cd77671bd8..adbdfa13d6b 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -15,14 +15,14 @@ "confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f {name}." }, @@ -30,7 +30,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AVM FRITZ!Box." } diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 933dd797dfc..0ba0de59849 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -21,11 +21,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up the fritzbox_callmonitor integration.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up the fritzbox_callmonitor platforms.""" fritzbox_phonebook = FritzBoxPhonebook( @@ -59,9 +54,9 @@ async def async_setup_entry(hass, config_entry): UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -73,8 +68,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py index ec62e196855..af0612d7632 100644 --- a/homeassistant/components/fritzbox_callmonitor/base.py +++ b/homeassistant/components/fritzbox_callmonitor/base.py @@ -1,4 +1,5 @@ """Base class for fritzbox_callmonitor entities.""" +from contextlib import suppress from datetime import timedelta import logging import re @@ -69,11 +70,7 @@ class FritzBoxPhonebook: return UNKOWN_NAME for prefix in self.prefixes: - try: + with suppress(KeyError): return self.number_dict[prefix + number] - except KeyError: - pass - try: + with suppress(KeyError): return self.number_dict[prefix + number.lstrip("0")] - except KeyError: - pass diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index 01a43f7c7ef..c4d0076a792 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - ATTR_NAME, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -17,8 +16,6 @@ from homeassistant.const import ( from homeassistant.core import callback from .base import FritzBoxPhonebook - -# pylint:disable=unused-import from .const import ( CONF_PHONEBOOK, CONF_PREFIXES, @@ -28,6 +25,7 @@ from .const import ( DEFAULT_USERNAME, DOMAIN, FRITZ_ACTION_GET_INFO, + FRITZ_ATTR_NAME, FRITZ_ATTR_SERIAL_NUMBER, FRITZ_SERVICE_DEVICE_INFO, SERIAL_NUMBER, @@ -119,7 +117,7 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): phonebook_info = await self.hass.async_add_executor_job( self._fritzbox_phonebook.fph.phonebook_info, phonebook_id ) - return phonebook_info[ATTR_NAME] + return phonebook_info[FRITZ_ATTR_NAME] async def _get_list_of_phonebook_names(self): """Return list of names for all available phonebooks.""" diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index 6f0c87f5273..a71f14401b3 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -15,6 +15,7 @@ ICON_PHONE = "mdi:phone" ATTR_PREFIXES = "prefixes" FRITZ_ACTION_GET_INFO = "GetInfo" +FRITZ_ATTR_NAME = "name" FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber" FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo" diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 891bf8131d6..a325c0ca71d 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -8,7 +8,7 @@ from time import sleep from fritzconnection.core.fritzmonitor import FritzMonitor import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, @@ -19,7 +19,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from .const import ( ATTR_PREFIXES, @@ -102,7 +101,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([sensor]) -class FritzBoxCallSensor(Entity): +class FritzBoxCallSensor(SensorEntity): """Implementation of a Fritz!Box call monitor.""" def __init__(self, name, unique_id, fritzbox_phonebook, prefixes, host, port): @@ -169,7 +168,7 @@ class FritzBoxCallSensor(Entity): return ICON_PHONE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._prefixes: self._attributes[ATTR_PREFIXES] = self._prefixes diff --git a/homeassistant/components/fritzbox_callmonitor/translations/bg.json b/homeassistant/components/fritzbox_callmonitor/translations/bg.json new file mode 100644 index 00000000000..fc2115d9ca0 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/hu.json b/homeassistant/components/fritzbox_callmonitor/translations/hu.json new file mode 100644 index 00000000000..8c2c34775e5 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "phonebook": { + "data": { + "phonebook": "Telefonk\u00f6nyv" + } + }, + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/id.json b/homeassistant/components/fritzbox_callmonitor/translations/id.json new file mode 100644 index 00000000000..43bb4a16b47 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "insufficient_permissions": "Izin pengguna tidak memadai untuk mengakses pengaturan dan buku telepon AVM FRITZ!Box.", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "Pantau panggilan AVM FRITZ!Box: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Buku telepon" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Format prefiks salah, periksa formatnya." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefiks (daftar yang dipisahkan koma)" + }, + "title": "Konfigurasikan Prefiks" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ko.json b/homeassistant/components/fritzbox_callmonitor/translations/ko.json index b8fd442cd03..51f4ccec35a 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ko.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ko.json @@ -2,12 +2,19 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "insufficient_permissions": "AVM FRIZ!Box \uc124\uc815 \ubc0f \uc804\ud654\ubc88\ud638\ubd80\uc5d0 \ud544\uc694\ud55c \uc0ac\uc6a9\uc790 \uc811\uadfc \uad8c\ud55c\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4.", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "AVM FRITZ!Box \uc804\ud654 \ubaa8\ub2c8\ud130\ub9c1: {name}", "step": { + "phonebook": { + "data": { + "phonebook": "\uc804\ud654\ubc88\ud638\ubd80" + } + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", @@ -17,5 +24,18 @@ } } } + }, + "options": { + "error": { + "malformed_prefixes": "\uc811\ub450\uc0ac\uc758 \ud615\uc2dd\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud615\uc2dd\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "init": { + "data": { + "prefixes": "\uc811\ub450\uc0ac (\uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ubaa9\ub85d)" + }, + "title": "\uc811\ub450\uc0ac \uad6c\uc131\ud558\uae30" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/nl.json b/homeassistant/components/fritzbox_callmonitor/translations/nl.json index 3381ed0d9b2..bc706861313 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/nl.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/nl.json @@ -2,12 +2,19 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "insufficient_permissions": "Gebruiker heeft onvoldoende rechten voor toegang tot AVM FRITZ!Box instellingen en de telefoonboeken.", "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { "invalid_auth": "Ongeldige authenticatie" }, + "flow_title": "AVM FRITZ!Box oproepmonitor: {name}", "step": { + "phonebook": { + "data": { + "phonebook": "Telefoonboek" + } + }, "user": { "data": { "host": "Host", @@ -17,5 +24,18 @@ } } } + }, + "options": { + "error": { + "malformed_prefixes": "Voorvoegsels hebben een onjuiste indeling, controleer hun indeling." + }, + "step": { + "init": { + "data": { + "prefixes": "Voorvoegsels (door komma's gescheiden lijst)" + }, + "title": "Configureer voorvoegsels" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ru.json b/homeassistant/components/fritzbox_callmonitor/translations/ru.json index f1bcb18a2f6..38448ac8c59 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/ru.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/ru.json @@ -20,7 +20,7 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 13b822ae8a4..3c37de7664c 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -7,10 +7,9 @@ from fritzconnection.lib.fritzstatus import FritzStatus from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([FritzboxMonitorSensor(name, fstatus)], True) -class FritzboxMonitorSensor(Entity): +class FritzboxMonitorSensor(SensorEntity): """Implementation of a fritzbox monitor sensor.""" def __init__(self, name, fstatus): @@ -93,7 +92,7 @@ class FritzboxMonitorSensor(Entity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" # Don't return attributes if FritzBox is unreachable if self._state == STATE_UNAVAILABLE: diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 02ff760e574..a908f2605f8 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,13 +1,14 @@ """Support for Fronius devices.""" +from __future__ import annotations + import copy from datetime import timedelta import logging -from typing import Dict from pyfronius import Fronius import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DEVICE, CONF_MONITORED_CONDITIONS, @@ -17,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -195,7 +195,7 @@ class FroniusAdapter: for sensor in self._registered_sensors: sensor.async_schedule_update_ha_state(True) - async def _update(self) -> Dict: + async def _update(self) -> dict: """Return values of interest.""" async def register(self, sensor): @@ -251,7 +251,7 @@ class FroniusPowerFlow(FroniusAdapter): return await self.bridge.current_power_flow() -class FroniusTemplateSensor(Entity): +class FroniusTemplateSensor(SensorEntity): """Sensor for the single values (e.g. pv power, ac power).""" def __init__(self, parent: FroniusAdapter, name): diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index bb503ee8673..20369503f5c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,10 +1,12 @@ """Handle the frontend for Home Assistant.""" +from __future__ import annotations + import json import logging import mimetypes import os import pathlib -from typing import Any, Dict, Optional, Set, Tuple +from typing import Any from aiohttp import hdrs, web, web_urldispatcher import jinja2 @@ -57,6 +59,13 @@ MANIFEST_JSON = { } for size in (192, 384, 512, 1024) ], + "screenshots": [ + { + "src": "/static/images/screenshots/screenshot-1.png", + "sizes": "413×792", + "type": "image/png", + } + ], "lang": "en-US", "name": "Home Assistant", "short_name": "Assistant", @@ -119,19 +128,19 @@ class Panel: """Abstract class for panels.""" # Name of the webcomponent - component_name: Optional[str] = None + component_name: str | None = None # Icon to show in the sidebar - sidebar_icon: Optional[str] = None + sidebar_icon: str | None = None # Title to show in the sidebar - sidebar_title: Optional[str] = None + sidebar_title: str | None = None # Url to show the panel in the frontend - frontend_url_path: Optional[str] = None + frontend_url_path: str | None = None # Config to pass to the webcomponent - config: Optional[Dict[str, Any]] = None + config: dict[str, Any] | None = None # If the panel should only be visible to admins require_admin = False @@ -443,7 +452,7 @@ class IndexView(web_urldispatcher.AbstractResource): async def resolve( self, request: web.Request - ) -> Tuple[Optional[web_urldispatcher.UrlMappingMatchInfo], Set[str]]: + ) -> tuple[web_urldispatcher.UrlMappingMatchInfo | None, set[str]]: """Resolve resource. Return (UrlMappingMatchInfo, allowed_methods) pair. diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 694be0382f7..b910c0acc46 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==20210302.6" + "home-assistant-frontend==20210407.1" ], "dependencies": [ "api", diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index 9c089e85811..ad89c3a035b 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -120,9 +120,8 @@ class GaradgetCover(CoverEntity): def __del__(self): """Try to remove token.""" - if self._obtained_token is True: - if self.access_token is not None: - self.remove_token() + if self._obtained_token is True and self.access_token is not None: + self.remove_token() @property def name(self): @@ -135,7 +134,7 @@ class GaradgetCover(CoverEntity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" data = {} @@ -239,10 +238,12 @@ class GaradgetCover(CoverEntity): ) self._state = STATE_OFFLINE - if self._state not in [STATE_CLOSING, STATE_OPENING]: - if self._unsub_listener_cover is not None: - self._unsub_listener_cover() - self._unsub_listener_cover = None + if ( + self._state not in [STATE_CLOSING, STATE_OPENING] + and self._unsub_listener_cover is not None + ): + self._unsub_listener_cover() + self._unsub_listener_cover = None def _get_variable(self, var): """Get latest status.""" diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 896c243f386..c009124b024 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -57,9 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): garmin_data = GarminConnectData(hass, garmin_client) hass.data[DOMAIN][entry.entry_id] = garmin_data - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -70,8 +70,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py index e0b50fa371b..7cd2309214a 100644 --- a/homeassistant/components/garmin_connect/config_flow.py +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 5d18f0a0dd0..46db6e615f1 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -1,6 +1,8 @@ """Platform for Garmin Connect integration.""" +from __future__ import annotations + import logging -from typing import Any, Dict +from typing import Any from garminconnect import ( GarminConnectAuthenticationError, @@ -8,9 +10,9 @@ from garminconnect import ( GarminConnectTooManyRequestsError, ) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from .alarm_util import calculate_next_active_alarms @@ -68,7 +70,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class GarminConnectSensor(Entity): +class GarminConnectSensor(SensorEntity): """Representation of a Garmin Connect Sensor.""" def __init__( @@ -120,7 +122,7 @@ class GarminConnectSensor(Entity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return attributes for sensor.""" if not self._data.data: return {} @@ -136,7 +138,7 @@ class GarminConnectSensor(Entity): return attributes @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information.""" return { "identifiers": {(DOMAIN, self._unique_id)}, diff --git a/homeassistant/components/garmin_connect/translations/he.json b/homeassistant/components/garmin_connect/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/hu.json b/homeassistant/components/garmin_connect/translations/hu.json index 2ada884847f..ae518acf001 100644 --- a/homeassistant/components/garmin_connect/translations/hu.json +++ b/homeassistant/components/garmin_connect/translations/hu.json @@ -4,10 +4,10 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.", - "unknown": "V\u00e1ratlan hiba." + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/garmin_connect/translations/id.json b/homeassistant/components/garmin_connect/translations/id.json new file mode 100644 index 00000000000..27460757234 --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial Anda.", + "title": "Garmin Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/nl.json b/homeassistant/components/garmin_connect/translations/nl.json index e9b71c49c71..e751aaf1b5c 100644 --- a/homeassistant/components/garmin_connect/translations/nl.json +++ b/homeassistant/components/garmin_connect/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dit account is al geconfigureerd." + "already_configured": "Account is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw.", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "too_many_requests": "Te veel aanvragen, probeer het later opnieuw.", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/garmin_connect/translations/ru.json b/homeassistant/components/garmin_connect/translations/ru.json index 49dd5c5b3bc..066c337309f 100644 --- a/homeassistant/components/garmin_connect/translations/ru.json +++ b/homeassistant/components/garmin_connect/translations/ru.json @@ -13,7 +13,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Garmin Connect" diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index 77380afc2b5..51abd9d5693 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -25,7 +25,6 @@ CONFIG_SCHEMA = vol.Schema( ) -# pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" config = base_config[DOMAIN] diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py index 1e12a116ed5..b672b56ad9b 100644 --- a/homeassistant/components/gdacs/config_flow.py +++ b/homeassistant/components/gdacs/config_flow.py @@ -12,12 +12,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv -from .const import ( # pylint: disable=unused-import - CONF_CATEGORIES, - DEFAULT_RADIUS, - DEFAULT_SCAN_INTERVAL, - DOMAIN, -) +from .const import CONF_CATEGORIES, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN DATA_SCHEMA = vol.Schema( {vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int} diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 890c9f8e050..7e3dc7484bb 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -1,6 +1,7 @@ """Geolocation support for GDACS Feed.""" +from __future__ import annotations + import logging -from typing import Optional from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import ( @@ -169,7 +170,7 @@ class GdacsEvent(GeolocationEvent): self._version = feed_entry.version @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID containing latitude/longitude and external id.""" return f"{self._integration_id}_{self._external_id}" @@ -186,22 +187,22 @@ class GdacsEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._title @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -213,7 +214,7 @@ class GdacsEvent(GeolocationEvent): return LENGTH_KILOMETERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index fbbb199499b..2e4759088fc 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -1,10 +1,11 @@ """Feed Entity Manager Sensor support for GDACS Feed.""" -import logging -from typing import Optional +from __future__ import annotations +import logging + +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import dt from .const import DEFAULT_ICON, DOMAIN, FEED @@ -33,7 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Sensor setup done") -class GdacsSensor(Entity): +class GdacsSensor(SensorEntity): """This is a status sensor for the GDACS integration.""" def __init__(self, config_entry_id, config_unique_id, config_title, manager): @@ -109,12 +110,12 @@ class GdacsSensor(Entity): return self._total @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID containing latitude/longitude.""" return self._config_unique_id @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return f"GDACS ({self._config_title})" @@ -129,7 +130,7 @@ class GdacsSensor(Entity): return DEFAULT_UNIT_OF_MEASUREMENT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/gdacs/translations/hu.json b/homeassistant/components/gdacs/translations/hu.json index 47eca9a7fac..fefcabae802 100644 --- a/homeassistant/components/gdacs/translations/hu.json +++ b/homeassistant/components/gdacs/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van." + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "step": { "user": { diff --git a/homeassistant/components/gdacs/translations/id.json b/homeassistant/components/gdacs/translations/id.json new file mode 100644 index 00000000000..55e1db686a2 --- /dev/null +++ b/homeassistant/components/gdacs/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Isi detail filter Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/nl.json b/homeassistant/components/gdacs/translations/nl.json index f2a09892a66..6dd3e5aa196 100644 --- a/homeassistant/components/gdacs/translations/nl.json +++ b/homeassistant/components/gdacs/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Service is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/gdacs/translations/zh-Hant.json b/homeassistant/components/gdacs/translations/zh-Hant.json index 164d90fdcfd..e5c6dfe6e50 100644 --- a/homeassistant/components/gdacs/translations/zh-Hant.json +++ b/homeassistant/components/gdacs/translations/zh-Hant.json @@ -8,7 +8,7 @@ "data": { "radius": "\u534a\u5f91" }, - "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + "title": "\u586b\u5beb\u7be9\u9078\u5668\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index 43e41e25e5e..94d329a417e 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -4,13 +4,11 @@ from datetime import timedelta from geizhals import Device, Geizhals import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +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.helpers.entity import Entity from homeassistant.util import Throttle -CONF_DESCRIPTION = "description" CONF_PRODUCT_ID = "product_id" CONF_LOCALE = "locale" @@ -38,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([Geizwatch(name, description, product_id, domain)], True) -class Geizwatch(Entity): +class Geizwatch(SensorEntity): """Implementation of Geizwatch.""" def __init__(self, name, description, product_id, domain): @@ -72,7 +70,7 @@ class Geizwatch(Entity): return self._device.prices[0] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" while len(self._device.prices) < 4: self._device.prices.append("None") diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 56b490e165a..1ec7f0874e0 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -125,32 +125,45 @@ class GenericCamera(Camera): ).result() async def async_camera_image(self): + """Wrap _async_camera_image with an asyncio.shield.""" + # Shield the request because of https://github.com/encode/httpx/issues/1461 + try: + self._last_url, self._last_image = await asyncio.shield( + self._async_camera_image() + ) + except asyncio.CancelledError as err: + _LOGGER.warning("Timeout getting camera image from %s", self._name) + raise err + return self._last_image + + async def _async_camera_image(self): """Return a still image response from the camera.""" try: url = self._still_image_url.async_render(parse_result=False) except TemplateError as err: _LOGGER.error("Error parsing template %s: %s", self._still_image_url, err) - return self._last_image + return self._last_url, self._last_image if url == self._last_url and self._limit_refetch: - return self._last_image - + return self._last_url, self._last_image + response = None try: async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl) response = await async_client.get( url, auth=self._auth, timeout=GET_IMAGE_TIMEOUT ) response.raise_for_status() - self._last_image = response.content + image = response.content except httpx.TimeoutException: _LOGGER.error("Timeout getting camera image from %s", self._name) - return self._last_image + return self._last_url, self._last_image except (httpx.RequestError, httpx.HTTPStatusError) as err: _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) - return self._last_image - - self._last_url = url - return self._last_image + return self._last_url, self._last_image + finally: + if response: + await response.aclose() + return url, image @property def name(self): diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 7062267de19..e83852d122f 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -1,6 +1,7 @@ """Adds support for generic thermostat units.""" import asyncio import logging +import math import voluptuous as vol @@ -266,6 +267,14 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._hvac_mode: self._hvac_mode = HVAC_MODE_OFF + # Prevent the device from keep running if HVAC_MODE_OFF + if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active: + await self._async_heater_turn_off() + _LOGGER.warning( + "The climate mode is OFF, but the switch device is ON. Turning off device %s", + self.heater_entity_id, + ) + @property def should_poll(self): """Return the polling state.""" @@ -411,14 +420,21 @@ class GenericThermostat(ClimateEntity, RestoreEntity): def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" try: - self._cur_temp = float(state.state) + cur_temp = float(state.state) + if math.isnan(cur_temp) or math.isinf(cur_temp): + raise ValueError(f"Sensor has illegal state {state.state}") + self._cur_temp = cur_temp except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) async def _async_control_heating(self, time=None, force=False): """Check if we need to turn heating on or off.""" async with self._temp_lock: - if not self._active and None not in (self._cur_temp, self._target_temp): + if not self._active and None not in ( + self._cur_temp, + self._target_temp, + self._is_device_active, + ): self._active = True _LOGGER.info( "Obtained current and target temperature. " @@ -430,28 +446,27 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._active or self._hvac_mode == HVAC_MODE_OFF: return - if not force and time is None: - # If the `force` argument is True, we - # ignore `min_cycle_duration`. - # If the `time` argument is not none, we were invoked for - # keep-alive purposes, and `min_cycle_duration` is irrelevant. - if self.min_cycle_duration: - if self._is_device_active: - current_state = STATE_ON - else: - current_state = HVAC_MODE_OFF - try: - long_enough = condition.state( - self.hass, - self.heater_entity_id, - current_state, - self.min_cycle_duration, - ) - except ConditionError: - long_enough = False + # If the `force` argument is True, we + # ignore `min_cycle_duration`. + # If the `time` argument is not none, we were invoked for + # keep-alive purposes, and `min_cycle_duration` is irrelevant. + if not force and time is None and self.min_cycle_duration: + if self._is_device_active: + current_state = STATE_ON + else: + current_state = HVAC_MODE_OFF + try: + long_enough = condition.state( + self.hass, + self.heater_entity_id, + current_state, + self.min_cycle_duration, + ) + except ConditionError: + long_enough = False - if not long_enough: - return + if not long_enough: + return too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance @@ -480,6 +495,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): @property def _is_device_active(self): """If the toggleable device is currently active.""" + if not self.hass.states.get(self.heater_entity_id): + return None + return self.hass.states.is_state(self.heater_entity_id, STATE_ON) @property diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 909de81521c..f1d2a1d47c1 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,7 +1,9 @@ """Support for a Genius Hub system.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Dict, Optional +from typing import Any import aiohttp from geniushubclient import GeniusHub @@ -173,7 +175,6 @@ class GeniusBroker: @property def hub_uid(self) -> int: """Return the Hub UID (MAC address).""" - # pylint: disable=no-member return self._hub_uid if self._hub_uid is not None else self.client.uid async def async_update(self, now, **kwargs) -> None: @@ -219,12 +220,12 @@ class GeniusEntity(Entity): """Set up a listener when this entity is added to HA.""" self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) - async def _refresh(self, payload: Optional[dict] = None) -> None: + async def _refresh(self, payload: dict | None = None) -> None: """Process any signals.""" self.async_schedule_update_ha_state(force_refresh=True) @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._unique_id @@ -251,7 +252,7 @@ class GeniusDevice(GeniusEntity): self._last_comms = self._state_attr = None @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attrs = {} attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] @@ -286,7 +287,7 @@ class GeniusZone(GeniusEntity): self._zone = zone self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" - async def _refresh(self, payload: Optional[dict] = None) -> None: + async def _refresh(self, payload: dict | None = None) -> None: """Process any signals.""" if payload is None: self.async_schedule_update_ha_state(force_refresh=True) @@ -318,7 +319,7 @@ class GeniusZone(GeniusEntity): return self._zone.name @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} return {"status": status} @@ -334,7 +335,7 @@ class GeniusHeatingZone(GeniusZone): self._max_temp = self._min_temp = self._supported_features = None @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._zone.data.get("temperature") diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 70d08dc2d1f..089fd964835 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,5 @@ """Support for Genius Hub climate devices.""" -from typing import List, Optional +from __future__ import annotations from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -67,12 +67,12 @@ class GeniusClimateZone(GeniusHeatingZone, ClimateEntity): return GH_HVAC_TO_HA.get(self._zone.data["mode"], HVAC_MODE_HEAT) @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return list(HA_HVAC_TO_GH) @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" if "_state" in self._zone.data: # only for v3 API if not self._zone.data["_state"].get("bIsActive"): @@ -83,12 +83,12 @@ class GeniusClimateZone(GeniusHeatingZone, ClimateEntity): return None @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" return GH_PRESET_TO_HA.get(self._zone.data["mode"]) @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" if "occupied" in self._zone.data: # if has a movement sensor return [PRESET_ACTIVITY, PRESET_BOOST] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 7e4fe81fc77..3234ccd577f 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,7 +1,10 @@ """Support for Genius Hub sensor devices.""" -from datetime import timedelta -from typing import Any, Dict +from __future__ import annotations +from datetime import timedelta +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 import homeassistant.util.dt as dt_util @@ -36,7 +39,7 @@ async def async_setup_platform( async_add_entities(sensors + issues, update_before_add=True) -class GeniusBattery(GeniusDevice): +class GeniusBattery(GeniusDevice, SensorEntity): """Representation of a Genius Hub sensor.""" def __init__(self, broker, device, state_attr) -> None: @@ -86,7 +89,7 @@ class GeniusBattery(GeniusDevice): return level if level != 255 else 0 -class GeniusIssue(GeniusEntity): +class GeniusIssue(GeniusEntity, SensorEntity): """Representation of a Genius Hub sensor.""" def __init__(self, broker, level) -> None: @@ -106,7 +109,7 @@ class GeniusIssue(GeniusEntity): return len(self._issues) @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return {f"{self._level}_list": self._issues} diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 51fdce4a6d7..bb775432d8e 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,5 +1,5 @@ """Support for Genius Hub water_heater devices.""" -from typing import List +from __future__ import annotations from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, @@ -61,7 +61,7 @@ class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterEntity): self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @property - def operation_list(self) -> List[str]: + def operation_list(self) -> list[str]: """Return the list of available operation modes.""" return list(HA_OPMODE_TO_GH) diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 40386648138..cfd58124c16 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -1,7 +1,8 @@ """Support for generic GeoJSON events.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional from geojson_client.generic_feed import GenericFeedManager import voluptuous as vol @@ -176,22 +177,22 @@ class GeoJsonLocationEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -201,7 +202,7 @@ class GeoJsonLocationEvent(GeolocationEvent): return LENGTH_KILOMETERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if not self._external_id: return {} diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 6142fa22209..11294e73f63 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -1,7 +1,9 @@ """Support for Geolocation.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional +from typing import final from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -45,7 +47,7 @@ async def async_unload_entry(hass, entry): class GeolocationEvent(Entity): - """This represents an external event with an associated geolocation.""" + """Base class for an external event with an associated geolocation.""" @property def state(self): @@ -60,20 +62,21 @@ class GeolocationEvent(Entity): raise NotImplementedError @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return None @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return None @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return None + @final @property def state_attributes(self): """Return the state attributes of this external event.""" diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 9ca8c86e150..3cdefccfeab 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -33,6 +33,7 @@ def source_match(state, source): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) @@ -74,6 +75,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "zone": zone_state, "event": trigger_event, "description": f"geo_location - {source}", + "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index c75234f5f2b..df5f11850fd 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -12,7 +12,7 @@ from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA from georss_generic_client import GenericFeed import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -23,7 +23,6 @@ from homeassistant.const import ( LENGTH_KILOMETERS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -96,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class GeoRssServiceSensor(Entity): +class GeoRssServiceSensor(SensorEntity): """Representation of a Sensor.""" def __init__( @@ -137,7 +136,7 @@ class GeoRssServiceSensor(Entity): return DEFAULT_ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._state_attributes diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index f090f516fb1..5a58e73d44a 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -56,7 +56,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): self._unique_id = device @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific attributes.""" return self._attributes diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index 99dc0fecead..826b943e2f8 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k." + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { diff --git a/homeassistant/components/geofency/translations/id.json b/homeassistant/components/geofency/translations/id.json new file mode 100644 index 00000000000..0e5163b96cd --- /dev/null +++ b/homeassistant/components/geofency/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di Geofency.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan Geofency Webhook?", + "title": "Siapkan Geofency Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ko.json b/homeassistant/components/geofency/translations/ko.json index fd49c57e249..b9303110e35 100644 --- a/homeassistant/components/geofency/translations/ko.json +++ b/homeassistant/components/geofency/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Geofency\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index f3f5829f465..735c6cd6d9f 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv -from .const import ( # pylint: disable=unused-import +from .const import ( CONF_MINIMUM_MAGNITUDE, CONF_MMI, DEFAULT_MINIMUM_MAGNITUDE, diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 718b4c06b9c..1643264fd75 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,6 +1,7 @@ """Geolocation support for GeoNet NZ Quakes Feeds.""" +from __future__ import annotations + import logging -from typing import Optional from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import ( @@ -142,7 +143,7 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._time = feed_entry.time @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID containing latitude/longitude and external id.""" return f"{self._integration_id}_{self._external_id}" @@ -157,22 +158,22 @@ class GeonetnzQuakesEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._title @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -184,7 +185,7 @@ class GeonetnzQuakesEvent(GeolocationEvent): return LENGTH_KILOMETERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index 1cb2d0dc091..94c7965663a 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -1,10 +1,11 @@ """Feed Entity Manager Sensor support for GeoNet NZ Quakes Feeds.""" -import logging -from typing import Optional +from __future__ import annotations +import logging + +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import dt from .const import DOMAIN, FEED @@ -34,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Sensor setup done") -class GeonetnzQuakesSensor(Entity): +class GeonetnzQuakesSensor(SensorEntity): """This is a status sensor for the GeoNet NZ Quakes integration.""" def __init__(self, config_entry_id, config_unique_id, config_title, manager): @@ -115,7 +116,7 @@ class GeonetnzQuakesSensor(Entity): return self._config_unique_id @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return f"GeoNet NZ Quakes ({self._config_title})" @@ -130,7 +131,7 @@ class GeonetnzQuakesSensor(Entity): return DEFAULT_UNIT_OF_MEASUREMENT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/geonetnz_quakes/translations/hu.json b/homeassistant/components/geonetnz_quakes/translations/hu.json index 4a163d24b75..21a38c18e28 100644 --- a/homeassistant/components/geonetnz_quakes/translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/translations/id.json b/homeassistant/components/geonetnz_quakes/translations/id.json new file mode 100644 index 00000000000..7a4e340e230 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Isi detail filter Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/nl.json b/homeassistant/components/geonetnz_quakes/translations/nl.json index 865860a5adf..74766300e11 100644 --- a/homeassistant/components/geonetnz_quakes/translations/nl.json +++ b/homeassistant/components/geonetnz_quakes/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Service is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json index f022792121c..dd6e15b82d1 100644 --- a/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_quakes/translations/zh-Hant.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "\u534a\u5f91" }, - "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + "title": "\u586b\u5beb\u7be9\u9078\u5668\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index e2c6cb77083..c3db7770499 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -1,8 +1,9 @@ """The GeoNet NZ Volcano integration.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta import logging -from typing import Optional from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager import voluptuous as vol @@ -172,11 +173,11 @@ class GeonetnzVolcanoFeedEntityManager: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def last_update(self) -> Optional[datetime]: + def last_update(self) -> datetime | None: """Return the last update of this feed.""" return self._feed_manager.last_update - def last_update_successful(self) -> Optional[datetime]: + def last_update_successful(self) -> datetime | None: """Return the last successful update of this feed.""" return self._feed_manager.last_update_successful diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index 3d5d0681f02..c0cc6801437 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -1,7 +1,9 @@ """Feed Entity Manager Sensor support for GeoNet NZ Volcano Feeds.""" -import logging -from typing import Optional +from __future__ import annotations +import logging + +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -11,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import dt from homeassistant.util.unit_system import IMPERIAL_SYSTEM @@ -53,7 +54,7 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Sensor setup done") -class GeonetnzVolcanoSensor(Entity): +class GeonetnzVolcanoSensor(SensorEntity): """This represents an external event with GeoNet NZ Volcano feed data.""" def __init__(self, config_entry_id, feed_manager, external_id, unit_system): @@ -139,7 +140,7 @@ class GeonetnzVolcanoSensor(Entity): return DEFAULT_ICON @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return f"Volcano {self._title}" @@ -149,7 +150,7 @@ class GeonetnzVolcanoSensor(Entity): return "alert level" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/geonetnz_volcano/translations/hu.json b/homeassistant/components/geonetnz_volcano/translations/hu.json index 42de5a13142..dadc8142d7e 100644 --- a/homeassistant/components/geonetnz_volcano/translations/hu.json +++ b/homeassistant/components/geonetnz_volcano/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_volcano/translations/id.json b/homeassistant/components/geonetnz_volcano/translations/id.json new file mode 100644 index 00000000000..5dd4414ca62 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Isi detail filter Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/ko.json b/homeassistant/components/geonetnz_volcano/translations/ko.json index 1aeaf219288..6d743c3a18d 100644 --- a/homeassistant/components/geonetnz_volcano/translations/ko.json +++ b/homeassistant/components/geonetnz_volcano/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json b/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json index 9dd69261e59..9c0e9a3df1d 100644 --- a/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_volcano/translations/zh-Hant.json @@ -8,7 +8,7 @@ "data": { "radius": "\u534a\u5f91" }, - "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + "title": "\u586b\u5beb\u7be9\u9078\u5668\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 005cc4c9c26..f25f7e76f59 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -5,8 +5,6 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from gios import ApiError, Gios, InvalidSensorsData, NoStationError -from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -15,11 +13,6 @@ from .const import CONF_STATION_ID, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured GIOS.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up GIOS as config entry.""" station_id = config_entry.data[CONF_STATION_ID] @@ -28,10 +21,7 @@ async def async_setup_entry(hass, config_entry): websession = async_get_clientsession(hass) coordinator = GiosDataUpdateCoordinator(hass, websession, station_id) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index 2853570ce58..ab83191a1ac 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -131,7 +131,7 @@ class GiosAirQuality(CoordinatorEntity, AirQualityEntity): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" # Different measuring stations have different sets of sensors. We don't know # what data we will get. diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index dfef829c479..d2a9f8b73fc 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN # pylint:disable=unused-import +from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN DATA_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 468e22260b5..3f520525a5a 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIOŚ", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==0.1.5"], + "requirements": ["gios==0.2.1"], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/gios/translations/hu.json b/homeassistant/components/gios/translations/hu.json index 5702d3b33d2..b35904e9d76 100644 --- a/homeassistant/components/gios/translations/hu.json +++ b/homeassistant/components/gios/translations/hu.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "A GIO\u015a integr\u00e1ci\u00f3 ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz m\u00e1r konfigur\u00e1lva van." + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem lehet csatlakozni a GIO\u015a szerverhez.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_sensors_data": "\u00c9rv\u00e9nytelen \u00e9rz\u00e9kel\u0151k adatai ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz.", "wrong_station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja nem megfelel\u0151." }, "step": { "user": { "data": { - "name": "Az integr\u00e1ci\u00f3 neve", + "name": "N\u00e9v", "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja" }, "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios", diff --git a/homeassistant/components/gios/translations/id.json b/homeassistant/components/gios/translations/id.json new file mode 100644 index 00000000000..b32210c30d5 --- /dev/null +++ b/homeassistant/components/gios/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_sensors_data": "Data sensor tidak valid untuk stasiun pengukuran ini.", + "wrong_station_id": "ID stasiun pengukuran salah." + }, + "step": { + "user": { + "data": { + "name": "Nama", + "station_id": "ID stasiun pengukuran" + }, + "description": "Siapkan integrasi kualitas udara GIO\u015a (Inspektorat Jenderal Perlindungan Lingkungan Polandia). Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Inspektorat Jenderal Perlindungan Lingkungan Polandia)" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server GIO\u015a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ko.json b/homeassistant/components/gios/translations/ko.json index 7895dafe8ce..e462ef4e3b6 100644 --- a/homeassistant/components/gios/translations/ko.json +++ b/homeassistant/components/gios/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "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", @@ -18,5 +18,10 @@ "title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)" } } + }, + "system_health": { + "info": { + "can_reach_server": "GIO\u015a \uc11c\ubc84 \uc5f0\uacb0" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/nl.json b/homeassistant/components/gios/translations/nl.json index 09fddb56225..baac3c6dc77 100644 --- a/homeassistant/components/gios/translations/nl.json +++ b/homeassistant/components/gios/translations/nl.json @@ -1,22 +1,27 @@ { "config": { "abort": { - "already_configured": "GIO\u015a-integratie voor dit meetstation is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd." }, "error": { - "cannot_connect": "Kan geen verbinding maken met de GIO\u015a-server.", + "cannot_connect": "Kan geen verbinding maken", "invalid_sensors_data": "Ongeldige sensorgegevens voor dit meetstation.", "wrong_station_id": "ID van het meetstation is niet correct." }, "step": { "user": { "data": { - "name": "Naam van de integratie", + "name": "Naam", "station_id": "ID van het meetstation" }, "description": "GIO\u015a (Poolse hoofdinspectie van milieubescherming) luchtkwaliteitintegratie instellen. Als u hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Poolse hoofdinspectie van milieubescherming)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Bereik GIO\u015a server" + } } } \ No newline at end of file diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 80d05ae1b9c..c7812fa621d 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -5,7 +5,7 @@ import logging import github import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_NAME, CONF_ACCESS_TOKEN, @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_URL, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -72,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class GitHubSensor(Entity): +class GitHubSensor(SensorEntity): """Representation of a GitHub sensor.""" def __init__(self, github_data): @@ -119,7 +118,7 @@ class GitHubSensor(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = { ATTR_PATH: self._repository_path, diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index 9edbe9733a8..0b619853348 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -5,7 +5,7 @@ import logging from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_NAME, @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_URL, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -66,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([GitLabSensor(_gitlab_data, _name)], True) -class GitLabSensor(Entity): +class GitLabSensor(SensorEntity): """Representation of a GitLab sensor.""" def __init__(self, gitlab_data, name): @@ -99,7 +98,7 @@ class GitLabSensor(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index aff6dc17923..20b68b2e5a9 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -5,10 +5,9 @@ from gitterpy.client import GitterClient from gitterpy.errors import GitterRoomError, GitterTokenError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([GitterSensor(gitter, room, name, username)], True) -class GitterSensor(Entity): +class GitterSensor(SensorEntity): """Representation of a Gitter sensor.""" def __init__(self, data, room, name, username): @@ -76,7 +75,7 @@ class GitterSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_USERNAME: self._username, diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 69e4ce0c016..18865a232d7 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -36,9 +36,10 @@ SENSOR_TYPES = { "process_thread": ["processcount", "Thread", "Count", CPU_ICON], "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON], "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON], - "temperature_core": ["sensors", "temperature", TEMP_CELSIUS, "mdi:thermometer"], - "fan_speed": ["sensors", "fan speed", "RPM", "mdi:fan"], - "battery": ["sensors", "charge", PERCENTAGE, "mdi:battery"], + "temperature_core": ["sensors", "Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_hdd": ["sensors", "Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "fan_speed": ["sensors", "Fan speed", "RPM", "mdi:fan"], + "battery": ["sensors", "Charge", PERCENTAGE, "mdi:battery"], "docker_active": ["docker", "Containers active", "", "mdi:docker"], "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker"], "docker_memory_use": [ diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 4c534a90ae1..bbe045eb232 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,8 +1,8 @@ """Support gathering system information of hosts which are running glances.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES @@ -15,52 +15,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities): dev = [] for sensor_type, sensor_details in SENSOR_TYPES.items(): - if not sensor_details[0] in client.api.data: + if sensor_details[0] not in client.api.data: continue - if sensor_details[0] in client.api.data: - if sensor_details[0] == "fs": - # fs will provide a list of disks attached - for disk in client.api.data[sensor_details[0]]: - dev.append( - GlancesSensor( - client, - name, - disk["mnt_point"], - SENSOR_TYPES[sensor_type][1], - sensor_type, - SENSOR_TYPES[sensor_type], - ) - ) - elif sensor_details[0] == "sensors": - # sensors will provide temp for different devices - for sensor in client.api.data[sensor_details[0]]: - if sensor["type"] == sensor_type: - dev.append( - GlancesSensor( - client, - name, - sensor["label"], - SENSOR_TYPES[sensor_type][1], - sensor_type, - SENSOR_TYPES[sensor_type], - ) - ) - elif client.api.data[sensor_details[0]]: + if sensor_details[0] == "fs": + # fs will provide a list of disks attached + for disk in client.api.data[sensor_details[0]]: dev.append( GlancesSensor( client, name, - "", + disk["mnt_point"], SENSOR_TYPES[sensor_type][1], sensor_type, SENSOR_TYPES[sensor_type], ) ) + elif sensor_details[0] == "sensors": + # sensors will provide temp for different devices + for sensor in client.api.data[sensor_details[0]]: + if sensor["type"] == sensor_type: + dev.append( + GlancesSensor( + client, + name, + sensor["label"], + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) + elif client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + "", + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) async_add_entities(dev, True) -class GlancesSensor(Entity): +class GlancesSensor(SensorEntity): """Implementation of a Glances sensor.""" def __init__( @@ -139,100 +138,103 @@ class GlancesSensor(Entity): if value is None: return - if value is not None: - if self.sensor_details[0] == "fs": - for var in value["fs"]: - if var["mnt_point"] == self._sensor_name_prefix: - disk = var - break - if self.type == "disk_use_percent": - self._state = disk["percent"] - elif self.type == "disk_use": - self._state = round(disk["used"] / 1024 ** 3, 1) - elif self.type == "disk_free": - try: - self._state = round(disk["free"] / 1024 ** 3, 1) - except KeyError: - self._state = round( - (disk["size"] - disk["used"]) / 1024 ** 3, - 1, - ) - elif self.type == "battery": - for sensor in value["sensors"]: - if sensor["type"] == "battery": - if sensor["label"] == self._sensor_name_prefix: - self._state = sensor["value"] - elif self.type == "fan_speed": - for sensor in value["sensors"]: - if sensor["type"] == "fan_speed": - if sensor["label"] == self._sensor_name_prefix: - self._state = sensor["value"] - elif self.type == "temperature_core": - for sensor in value["sensors"]: - if sensor["type"] == "temperature_core": - if sensor["label"] == self._sensor_name_prefix: - self._state = sensor["value"] - elif self.type == "memory_use_percent": - self._state = value["mem"]["percent"] - elif self.type == "memory_use": - self._state = round(value["mem"]["used"] / 1024 ** 2, 1) - elif self.type == "memory_free": - self._state = round(value["mem"]["free"] / 1024 ** 2, 1) - elif self.type == "swap_use_percent": - self._state = value["memswap"]["percent"] - elif self.type == "swap_use": - self._state = round(value["memswap"]["used"] / 1024 ** 3, 1) - elif self.type == "swap_free": - self._state = round(value["memswap"]["free"] / 1024 ** 3, 1) - elif self.type == "processor_load": - # Windows systems don't provide load details + if self.sensor_details[0] == "fs": + for var in value["fs"]: + if var["mnt_point"] == self._sensor_name_prefix: + disk = var + break + if self.type == "disk_free": try: - self._state = value["load"]["min15"] + self._state = round(disk["free"] / 1024 ** 3, 1) except KeyError: - self._state = value["cpu"]["total"] - elif self.type == "process_running": - self._state = value["processcount"]["running"] - elif self.type == "process_total": - self._state = value["processcount"]["total"] - elif self.type == "process_thread": - self._state = value["processcount"]["thread"] - elif self.type == "process_sleeping": - self._state = value["processcount"]["sleeping"] - elif self.type == "cpu_use_percent": - self._state = value["quicklook"]["cpu"] - elif self.type == "docker_active": - count = 0 - try: - for container in value["docker"]["containers"]: - if ( - container["Status"] == "running" - or "Up" in container["Status"] - ): - count += 1 - self._state = count - except KeyError: - self._state = count - elif self.type == "docker_cpu_use": - cpu_use = 0.0 - try: - for container in value["docker"]["containers"]: - if ( - container["Status"] == "running" - or "Up" in container["Status"] - ): - cpu_use += container["cpu"]["total"] - self._state = round(cpu_use, 1) - except KeyError: - self._state = STATE_UNAVAILABLE - elif self.type == "docker_memory_use": - mem_use = 0.0 - try: - for container in value["docker"]["containers"]: - if ( - container["Status"] == "running" - or "Up" in container["Status"] - ): - mem_use += container["memory"]["usage"] - self._state = round(mem_use / 1024 ** 2, 1) - except KeyError: - self._state = STATE_UNAVAILABLE + self._state = round( + (disk["size"] - disk["used"]) / 1024 ** 3, + 1, + ) + elif self.type == "disk_use": + self._state = round(disk["used"] / 1024 ** 3, 1) + elif self.type == "disk_use_percent": + self._state = disk["percent"] + elif self.type == "battery": + for sensor in value["sensors"]: + if ( + sensor["type"] == "battery" + and sensor["label"] == self._sensor_name_prefix + ): + self._state = sensor["value"] + elif self.type == "fan_speed": + for sensor in value["sensors"]: + if ( + sensor["type"] == "fan_speed" + and sensor["label"] == self._sensor_name_prefix + ): + self._state = sensor["value"] + elif self.type == "temperature_core": + for sensor in value["sensors"]: + if ( + sensor["type"] == "temperature_core" + and sensor["label"] == self._sensor_name_prefix + ): + self._state = sensor["value"] + elif self.type == "temperature_hdd": + for sensor in value["sensors"]: + if ( + sensor["type"] == "temperature_hdd" + and sensor["label"] == self._sensor_name_prefix + ): + self._state = sensor["value"] + elif self.type == "memory_use_percent": + self._state = value["mem"]["percent"] + elif self.type == "memory_use": + self._state = round(value["mem"]["used"] / 1024 ** 2, 1) + elif self.type == "memory_free": + self._state = round(value["mem"]["free"] / 1024 ** 2, 1) + elif self.type == "swap_use_percent": + self._state = value["memswap"]["percent"] + elif self.type == "swap_use": + self._state = round(value["memswap"]["used"] / 1024 ** 3, 1) + elif self.type == "swap_free": + self._state = round(value["memswap"]["free"] / 1024 ** 3, 1) + elif self.type == "processor_load": + # Windows systems don't provide load details + try: + self._state = value["load"]["min15"] + except KeyError: + self._state = value["cpu"]["total"] + elif self.type == "process_running": + self._state = value["processcount"]["running"] + elif self.type == "process_total": + self._state = value["processcount"]["total"] + elif self.type == "process_thread": + self._state = value["processcount"]["thread"] + elif self.type == "process_sleeping": + self._state = value["processcount"]["sleeping"] + elif self.type == "cpu_use_percent": + self._state = value["quicklook"]["cpu"] + elif self.type == "docker_active": + count = 0 + try: + for container in value["docker"]["containers"]: + if container["Status"] == "running" or "Up" in container["Status"]: + count += 1 + self._state = count + except KeyError: + self._state = count + elif self.type == "docker_cpu_use": + cpu_use = 0.0 + try: + for container in value["docker"]["containers"]: + if container["Status"] == "running" or "Up" in container["Status"]: + cpu_use += container["cpu"]["total"] + self._state = round(cpu_use, 1) + except KeyError: + self._state = STATE_UNAVAILABLE + elif self.type == "docker_memory_use": + mem_use = 0.0 + try: + for container in value["docker"]["containers"]: + if container["Status"] == "running" or "Up" in container["Status"]: + mem_use += container["memory"]["usage"] + self._state = round(mem_use / 1024 ** 2, 1) + except KeyError: + self._state = STATE_UNAVAILABLE diff --git a/homeassistant/components/glances/translations/he.json b/homeassistant/components/glances/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/glances/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/hu.json b/homeassistant/components/glances/translations/hu.json index 0958efee4ae..d85baecb5ca 100644 --- a/homeassistant/components/glances/translations/hu.json +++ b/homeassistant/components/glances/translations/hu.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van." + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem lehet csatlakozni a kiszolg\u00e1l\u00f3hoz", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "wrong_version": "Nem t\u00e1mogatott verzi\u00f3 (2 vagy 3 csak)" }, "step": { @@ -14,9 +14,9 @@ "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", - "ssl": "Az SSL / TLS haszn\u00e1lat\u00e1val csatlakozzon a Glances rendszerhez", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "verify_ssl": "A rendszer tan\u00fas\u00edt\u00e1s\u00e1nak ellen\u0151rz\u00e9se", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se", "version": "Glances API-verzi\u00f3 (2 vagy 3)" }, "title": "Glances Be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/glances/translations/id.json b/homeassistant/components/glances/translations/id.json new file mode 100644 index 00000000000..13127e74322 --- /dev/null +++ b/homeassistant/components/glances/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "wrong_version": "Versi tidak didukung (hanya versi 2 atau versi 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL", + "version": "Versi API Glances (2 atau 3)" + }, + "title": "Siapkan Glances" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frekuensi pembaruan" + }, + "description": "Konfigurasikan opsi untuk Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/ko.json b/homeassistant/components/glances/translations/ko.json index 47f24d2edf1..e50206fade5 100644 --- a/homeassistant/components/glances/translations/ko.json +++ b/homeassistant/components/glances/translations/ko.json @@ -29,7 +29,7 @@ "data": { "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "description": "Glances \uc635\uc158 \uc124\uc815\ud558\uae30" + "description": "Glances\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/glances/translations/nl.json b/homeassistant/components/glances/translations/nl.json index c2f2b9d473a..6cb7fb445bb 100644 --- a/homeassistant/components/glances/translations/nl.json +++ b/homeassistant/components/glances/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Host is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken met host", + "cannot_connect": "Kan geen verbinding maken", "wrong_version": "Versie niet ondersteund (alleen 2 of 3)" }, "step": { @@ -14,9 +14,9 @@ "name": "Naam", "password": "Wachtwoord", "port": "Poort", - "ssl": "Gebruik SSL / TLS om verbinding te maken met het Glances-systeem", + "ssl": "Gebruik een SSL-certificaat", "username": "Gebruikersnaam", - "verify_ssl": "Controleer de certificering van het systeem", + "verify_ssl": "SSL-certificaat verifi\u00ebren", "version": "Glances API-versie (2 of 3)" }, "title": "Glances instellen" diff --git a/homeassistant/components/glances/translations/ru.json b/homeassistant/components/glances/translations/ru.json index 0dc8c72dc9f..aecffe204c8 100644 --- a/homeassistant/components/glances/translations/ru.json +++ b/homeassistant/components/glances/translations/ru.json @@ -15,7 +15,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", "version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)" }, diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index ff60a9ac043..e00b17ebae4 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -76,8 +76,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 35b6953865c..8a1333d6cf1 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEFAULT_NAME, DOMAIN # pylint:disable=unused-import +from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 7d8962cdb11..3916b987981 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -13,7 +13,8 @@ "data": { "host": "Host", "name": "Name" - } + }, + "description": "Zun\u00e4chst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/\n\nFolge den Anweisungen, um deinen Yeti mit deinem Wifi-Netzwerk zu verbinden. Bekomme dann die Host-IP von deinem Router. DHCP muss in den Router-Einstellungen f\u00fcr das Ger\u00e4t richtig eingerichtet werden, um sicherzustellen, dass sich die Host-IP nicht \u00e4ndert. Schaue hierzu im Benutzerhandbuch deines Routers nach." } } } diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json new file mode 100644 index 00000000000..c876a55301f --- /dev/null +++ b/homeassistant/components/goalzero/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "name": "N\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/id.json b/homeassistant/components/goalzero/translations/id.json new file mode 100644 index 00000000000..63fddf13a8e --- /dev/null +++ b/homeassistant/components/goalzero/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama" + }, + "description": "Pertama, Anda perlu mengunduh aplikasi Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nIkuti petunjuk untuk menghubungkan Yeti Anda ke jaringan Wi-Fi Anda. Kemudian dapatkan IP host dari router Anda. DHCP harus disetel di pengaturan router Anda untuk perangkat host agar IP host tidak berubah. Lihat manual pengguna router Anda.", + "title": "Goal Zero Yeti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/ko.json b/homeassistant/components/goalzero/translations/ko.json index f15f5827448..d5119363002 100644 --- a/homeassistant/components/goalzero/translations/ko.json +++ b/homeassistant/components/goalzero/translations/ko.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \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", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { @@ -13,7 +13,9 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" - } + }, + "description": "\uba3c\uc800 Goal Zero \uc571\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4. https://www.goalzero.com/product-features/yeti-app/\n\n\uc9c0\uce68\uc5d0 \ub530\ub77c Yeti\ub97c Wifi \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc5d0\uc11c \ud638\uc2a4\ud2b8 IP\ub97c \uac00\uc838\uc640\uc8fc\uc138\uc694. \ud638\uc2a4\ud2b8 IP\uac00 \ubcc0\uacbd\ub418\uc9c0 \uc54a\ub3c4\ub85d \ud558\ub824\uba74 \uae30\uae30\uc5d0 \ub300\ud574 \ub77c\uc6b0\ud130\uc5d0\uc11c DHCP\ub97c \uc54c\ub9de\uac8c \uc124\uc815\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud574\ub2f9 \ub0b4\uc6a9\uc5d0 \ub300\ud574\uc11c\ub294 \ub77c\uc6b0\ud130\uc758 \uc0ac\uc6a9\uc790 \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "Goal Zero Yeti" } } } diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index 4d9b5a397dd..c84ef7adb1f 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -14,7 +14,8 @@ "host": "Host", "name": "Naam" }, - "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om je Yeti te verbinden met je wifi-netwerk. Haal dan de host-ip van uw router. DHCP moet zijn ingesteld in uw routerinstellingen voor het apparaat om ervoor te zorgen dat het host-ip niet verandert. Raadpleeg de gebruikershandleiding van uw router." + "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om je Yeti te verbinden met je wifi-netwerk. Haal dan de host-ip van uw router. DHCP moet zijn ingesteld in uw routerinstellingen voor het apparaat om ervoor te zorgen dat het host-ip niet verandert. Raadpleeg de gebruikershandleiding van uw router.", + "title": "Goal Zero Yeti" } } } diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 93f000e6a3a..4c9e646c54d 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1,17 +1,16 @@ """The gogogate2 component.""" -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +import asyncio + +from homeassistant.components.cover import DOMAIN as COVER +from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .common import get_data_update_coordinator from .const import DEVICE_TYPE_GOGOGATE2 - -async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: - """Set up for Gogogate2 controllers.""" - return True +PLATFORMS = [COVER, SENSOR] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -29,22 +28,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.config_entries.async_update_entry(config_entry, **config_updates) data_update_coordinator = get_data_update_coordinator(hass, config_entry) - await data_update_coordinator.async_refresh() + await data_update_coordinator.async_config_entry_first_refresh() - if not data_update_coordinator.last_update_success: - raise ConfigEntryNotReady() - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, COVER_DOMAIN) - ) + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Gogogate2 config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) ) - return True + return unload_ok diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 2817c351013..e8b17184bbe 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -1,10 +1,12 @@ """Common code for GogoGate2 component.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Awaitable, Callable, NamedTuple, Optional +from typing import Awaitable, Callable, NamedTuple from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi -from gogogate2_api.common import AbstractDoor +from gogogate2_api.common import AbstractDoor, get_door_by_id from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -15,9 +17,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) -from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN +from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -26,8 +32,8 @@ class StateData(NamedTuple): """State data for a cover entity.""" config_unique_id: str - unique_id: Optional[str] - door: Optional[AbstractDoor] + unique_id: str | None + door: AbstractDoor | None class DeviceDataUpdateCoordinator(DataUpdateCoordinator): @@ -41,8 +47,8 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): *, name: str, update_interval: timedelta, - update_method: Optional[Callable[[], Awaitable]] = None, - request_refresh_debouncer: Optional[Debouncer] = None, + update_method: Callable[[], Awaitable] | None = None, + request_refresh_debouncer: Debouncer | None = None, ): """Initialize the data update coordinator.""" DataUpdateCoordinator.__init__( @@ -57,6 +63,45 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): self.api = api +class GoGoGate2Entity(CoordinatorEntity): + """Base class for gogogate2 entities.""" + + def __init__( + self, + config_entry: ConfigEntry, + data_update_coordinator: DeviceDataUpdateCoordinator, + door: AbstractDoor, + unique_id: str, + ) -> None: + """Initialize gogogate2 base entity.""" + super().__init__(data_update_coordinator) + self._config_entry = config_entry + self._door = door + self._unique_id = unique_id + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return self._unique_id + + def _get_door(self) -> AbstractDoor: + door = get_door_by_id(self._door.door_id, self.coordinator.data) + self._door = door or self._door + return self._door + + @property + def device_info(self): + """Device info for the controller.""" + data = self.coordinator.data + return { + "identifiers": {(DOMAIN, self._config_entry.unique_id)}, + "name": self._config_entry.title, + "manufacturer": MANUFACTURER, + "model": data.model, + "sw_version": data.firmwareversion, + } + + def get_data_update_coordinator( hass: HomeAssistant, config_entry: ConfigEntry ) -> DeviceDataUpdateCoordinator: @@ -95,6 +140,13 @@ def cover_unique_id(config_entry: ConfigEntry, door: AbstractDoor) -> str: return f"{config_entry.unique_id}_{door.door_id}" +def sensor_unique_id( + config_entry: ConfigEntry, door: AbstractDoor, sensor_type: str +) -> str: + """Generate a cover entity unique id.""" + return f"{config_entry.unique_id}_{door.door_id}_{sensor_type}" + + def get_api(config_data: dict) -> AbstractGateApi: """Get an api object for config data.""" gate_class = GogoGate2Api diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 0c3f1b3653c..3ecd3e85c3f 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -16,8 +16,7 @@ from homeassistant.const import ( ) from .common import get_api -from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE -from .const import DOMAIN # pylint: disable=unused-import +from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 8b83073d0c8..05fcb639e47 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,13 +1,10 @@ """Support for Gogogate2 garage Doors.""" -from typing import Callable, List, Optional +from __future__ import annotations -from gogogate2_api.common import ( - AbstractDoor, - DoorStatus, - get_configured_doors, - get_door_by_id, -) -import voluptuous as vol +import logging +from typing import Callable + +from gogogate2_api.common import AbstractDoor, DoorStatus, get_configured_doors from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, @@ -17,40 +14,28 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_DEVICE, - CONF_IP_ADDRESS, - CONF_PASSWORD, - CONF_USERNAME, -) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .common import ( DeviceDataUpdateCoordinator, + GoGoGate2Entity, cover_unique_id, get_data_update_coordinator, ) -from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN, MANUFACTURER +from .const import DOMAIN -COVER_SCHEMA = vol.Schema( - { - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_DEVICE, default=DEVICE_TYPE_GOGOGATE2): vol.In( - (DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE) - ), - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } -) +_LOGGER = logging.getLogger(__name__) async def async_setup_platform( hass: HomeAssistant, config: dict, add_entities: Callable, discovery_info=None ) -> None: """Convert old style file configs to new style configs.""" + _LOGGER.warning( + "Loading gogogate2 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 @@ -61,7 +46,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], Optional[bool]], None], + async_add_entities: Callable[[list[Entity], bool | None], None], ) -> None: """Set up the config entry.""" data_update_coordinator = get_data_update_coordinator(hass, config_entry) @@ -74,7 +59,7 @@ async def async_setup_entry( ) -class DeviceCover(CoordinatorEntity, CoverEntity): +class DeviceCover(GoGoGate2Entity, CoverEntity): """Cover entity for goggate2.""" def __init__( @@ -84,18 +69,11 @@ class DeviceCover(CoordinatorEntity, CoverEntity): door: AbstractDoor, ) -> None: """Initialize the object.""" - super().__init__(data_update_coordinator) - self._config_entry = config_entry - self._door = door + unique_id = cover_unique_id(config_entry, door) + super().__init__(config_entry, data_update_coordinator, door, unique_id) self._api = data_update_coordinator.api - self._unique_id = cover_unique_id(config_entry, door) self._is_available = True - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - @property def name(self): """Return the name of the door.""" @@ -136,25 +114,6 @@ class DeviceCover(CoordinatorEntity, CoverEntity): await self._api.async_close_door(self._get_door().door_id) @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - attrs = super().state_attributes - attrs["door_id"] = self._get_door().door_id - return attrs - - def _get_door(self) -> AbstractDoor: - door = get_door_by_id(self._door.door_id, self.coordinator.data) - self._door = door or self._door - return self._door - - @property - def device_info(self): - """Device info for the controller.""" - data = self.coordinator.data - return { - "identifiers": {(DOMAIN, self._config_entry.unique_id)}, - "name": self._config_entry.title, - "manufacturer": MANUFACTURER, - "model": data.model, - "sw_version": data.firmwareversion, - } + return {"door_id": self._get_door().door_id} diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py new file mode 100644 index 00000000000..9062bc0b352 --- /dev/null +++ b/homeassistant/components/gogogate2/sensor.py @@ -0,0 +1,130 @@ +"""Support for Gogogate2 garage Doors.""" +from __future__ import annotations + +from itertools import chain +from typing import Callable + +from gogogate2_api.common import AbstractDoor, get_configured_doors + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from .common import ( + DeviceDataUpdateCoordinator, + GoGoGate2Entity, + get_data_update_coordinator, + sensor_unique_id, +) + +SENSOR_ID_WIRED = "WIRE" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool | None], None], +) -> None: + """Set up the config entry.""" + data_update_coordinator = get_data_update_coordinator(hass, config_entry) + + sensors = chain( + [ + DoorSensorBattery(config_entry, data_update_coordinator, door) + for door in get_configured_doors(data_update_coordinator.data) + if door.sensorid and door.sensorid != SENSOR_ID_WIRED + ], + [ + DoorSensorTemperature(config_entry, data_update_coordinator, door) + for door in get_configured_doors(data_update_coordinator.data) + if door.sensorid and door.sensorid != SENSOR_ID_WIRED + ], + ) + async_add_entities(sensors) + + +class DoorSensorBattery(GoGoGate2Entity, SensorEntity): + """Battery sensor entity for gogogate2 door sensor.""" + + def __init__( + self, + config_entry: ConfigEntry, + data_update_coordinator: DeviceDataUpdateCoordinator, + door: AbstractDoor, + ) -> None: + """Initialize the object.""" + unique_id = sensor_unique_id(config_entry, door, "battery") + super().__init__(config_entry, data_update_coordinator, door, unique_id) + + @property + def name(self): + """Return the name of the door.""" + return f"{self._get_door().name} battery" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self): + """Return the state of the entity.""" + door = self._get_door() + return door.voltage # This is a percentage, not an absolute voltage + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + door = self._get_door() + if door.sensorid is not None: + return {"door_id": door.door_id, "sensor_id": door.sensorid} + return None + + +class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): + """Temperature sensor entity for gogogate2 door sensor.""" + + def __init__( + self, + config_entry: ConfigEntry, + data_update_coordinator: DeviceDataUpdateCoordinator, + door: AbstractDoor, + ) -> None: + """Initialize the object.""" + unique_id = sensor_unique_id(config_entry, door, "temperature") + super().__init__(config_entry, data_update_coordinator, door, unique_id) + + @property + def name(self): + """Return the name of the door.""" + return f"{self._get_door().name} temperature" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def state(self): + """Return the state of the entity.""" + door = self._get_door() + return door.temperature + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + door = self._get_door() + if door.sensorid is not None: + return {"door_id": door.door_id, "sensor_id": door.sensorid} + return None diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json index 952a502a72d..cdc76a4145a 100644 --- a/homeassistant/components/gogogate2/translations/hu.json +++ b/homeassistant/components/gogogate2/translations/hu.json @@ -10,9 +10,11 @@ "step": { "user": { "data": { + "ip_address": "IP c\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "A GogoGate2 vagy az iSmartGate be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/gogogate2/translations/id.json b/homeassistant/components/gogogate2/translations/id.json new file mode 100644 index 00000000000..9de61641d41 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Berikan informasi yang diperlukan di bawah ini.", + "title": "Siapkan GogoGate2 atau iSmartGate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index 43e9f7a1b2f..4efa554fc91 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -12,7 +12,7 @@ "data": { "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 GogoGate2 \u0438\u043b\u0438 iSmartGate" diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 2fcde78354b..2cc66121948 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,7 +3,7 @@ import copy from datetime import timedelta import logging -from httplib2 import ServerNotFoundError # pylint: disable=import-error +from httplib2 import ServerNotFoundError from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, @@ -80,7 +80,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): self.entity_id = entity_id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return {"offset_reached": self._offset_reached} diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 00c09242517..7793ed4d659 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,6 +1,8 @@ """Support for Actions on Google Assistant Smart Home Control.""" +from __future__ import annotations + import logging -from typing import Any, Dict +from typing import Any import voluptuous as vol @@ -87,7 +89,7 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): +async def async_setup(hass: HomeAssistant, yaml_config: dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index b4900d83b64..7eb69d08724 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,10 +1,11 @@ """Helper classes for Google Assistant integration.""" +from __future__ import annotations + from abc import ABC, abstractmethod from asyncio import gather from collections.abc import Mapping import logging import pprint -from typing import Dict, List, Optional, Tuple from aiohttp.web import json_response @@ -14,9 +15,10 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, + EVENT_HOMEASSISTANT_STARTED, STATE_UNAVAILABLE, ) -from homeassistant.core import Context, HomeAssistant, State, callback +from homeassistant.core import Context, CoreState, HomeAssistant, State, callback from homeassistant.helpers.area_registry import AreaEntry from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity_registry import RegistryEntry @@ -44,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) async def _get_entity_and_device( hass, entity_id -) -> Optional[Tuple[RegistryEntry, DeviceEntry]]: +) -> tuple[RegistryEntry, DeviceEntry] | None: """Fetch the entity and device entries for a entity_id.""" dev_reg, ent_reg = await gather( hass.helpers.device_registry.async_get_registry(), @@ -58,7 +60,7 @@ async def _get_entity_and_device( return entity_entry, device_entry -async def _get_area(hass, entity_entry, device_entry) -> Optional[AreaEntry]: +async def _get_area(hass, entity_entry, device_entry) -> AreaEntry | None: """Calculate the area for an entity.""" if entity_entry and entity_entry.area_id: area_id = entity_entry.area_id @@ -71,7 +73,7 @@ async def _get_area(hass, entity_entry, device_entry) -> Optional[AreaEntry]: return area_reg.areas.get(area_id) -async def _get_device_info(device_entry) -> Optional[Dict[str, str]]: +async def _get_device_info(device_entry) -> dict[str, str] | None: """Retrieve the device info for a device.""" if not device_entry: return None @@ -103,6 +105,16 @@ class AbstractConfig(ABC): self._store = GoogleConfigStore(self.hass) await self._store.async_load() + if self.hass.state == CoreState.running: + await self.async_sync_entities_all() + return + + async def sync_google(_): + """Sync entities to Google.""" + await self.async_sync_entities_all() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, sync_google) + @property def enabled(self): """Return if Google is enabled.""" @@ -192,7 +204,10 @@ class AbstractConfig(ABC): """Sync all entities to Google.""" # Remove any pending sync self._google_sync_unsub.pop(agent_user_id, lambda: None)() - return await self._async_request_sync_devices(agent_user_id) + status = await self._async_request_sync_devices(agent_user_id) + if status == 404: + await self.async_disconnect_agent_user(agent_user_id) + return status async def async_sync_entities_all(self): """Sync all entities to Google for all registered agents.""" @@ -254,13 +269,17 @@ class AbstractConfig(ABC): if webhook_id is None: return - webhook.async_register( - self.hass, - DOMAIN, - "Local Support", - webhook_id, - self._handle_local_webhook, - ) + try: + webhook.async_register( + self.hass, + DOMAIN, + "Local Support", + webhook_id, + self._handle_local_webhook, + ) + except ValueError: + _LOGGER.info("Webhook handler is already defined!") + return self._local_sdk_active = True @@ -344,7 +363,7 @@ class RequestData: user_id: str, source: str, request_id: str, - devices: Optional[List[dict]], + devices: list[dict] | None, ): """Initialize the request data.""" self.config = config @@ -578,7 +597,7 @@ def deep_update(target, source): @callback -def async_get_entities(hass, config) -> List[GoogleEntity]: +def async_get_entities(hass, config) -> list[GoogleEntity]: """Return all entities that are supported by Google.""" entities = [] for state in hass.states.async_all(): diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 5cf1cb14379..3787a63a514 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -135,11 +135,12 @@ class GoogleConfig(AbstractConfig): async def _async_request_sync_devices(self, agent_user_id: str): if CONF_SERVICE_ACCOUNT in self._config: - await self.async_call_homegraph_api( + return await self.async_call_homegraph_api( REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} ) - else: - _LOGGER.error("No configuration for request_sync available") + + _LOGGER.error("No configuration for request_sync available") + return HTTP_INTERNAL_SERVER_ERROR async def _async_update_token(self, force=False): if CONF_SERVICE_ACCOUNT not in self._config: diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 8b0bde09010..384c5bfd0ae 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,6 +1,7 @@ """Implement the Google Smart Home traits.""" +from __future__ import annotations + import logging -from typing import List, Optional from homeassistant.components import ( alarm_control_panel, @@ -27,6 +28,7 @@ from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_MODE, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CAST_APP_ID_HOMEASSISTANT, @@ -152,7 +154,7 @@ def _google_temp_unit(units): return "C" -def _next_selected(items: List[str], selected: Optional[str]) -> Optional[str]: +def _next_selected(items: list[str], selected: str | None) -> str | None: """Return the next item in a item list starting at given value. If selected is missing in items, None is returned @@ -762,7 +764,7 @@ class TemperatureSettingTrait(_Trait): mode in modes for mode in ("heatcool", "heat", "cool") ): modes.append("on") - response["availableThermostatModes"] = ",".join(modes) + response["availableThermostatModes"] = modes return response @@ -1424,11 +1426,10 @@ class ModesTrait(_Trait): elif self.state.domain == input_select.DOMAIN: mode_settings["option"] = self.state.state elif self.state.domain == humidifier.DOMAIN: - if humidifier.ATTR_MODE in attrs: - mode_settings["mode"] = attrs.get(humidifier.ATTR_MODE) - elif self.state.domain == light.DOMAIN: - if light.ATTR_EFFECT in attrs: - mode_settings["effect"] = attrs.get(light.ATTR_EFFECT) + if ATTR_MODE in attrs: + mode_settings["mode"] = attrs.get(ATTR_MODE) + elif self.state.domain == light.DOMAIN and light.ATTR_EFFECT in attrs: + mode_settings["effect"] = attrs.get(light.ATTR_EFFECT) if mode_settings: response["on"] = self.state.state not in (STATE_OFF, STATE_UNKNOWN) @@ -1460,7 +1461,7 @@ class ModesTrait(_Trait): humidifier.DOMAIN, humidifier.SERVICE_SET_MODE, { - humidifier.ATTR_MODE: requested_mode, + ATTR_MODE: requested_mode, ATTR_ENTITY_ID: self.state.entity_id, }, blocking=True, @@ -1616,15 +1617,17 @@ class OpenCloseTrait(_Trait): if self.state.domain == binary_sensor.DOMAIN: response["queryOnlyOpenClose"] = True response["discreteOnlyOpenClose"] = True - elif self.state.domain == cover.DOMAIN: - if features & cover.SUPPORT_SET_POSITION == 0: - response["discreteOnlyOpenClose"] = True + elif ( + self.state.domain == cover.DOMAIN + and features & cover.SUPPORT_SET_POSITION == 0 + ): + response["discreteOnlyOpenClose"] = True - if ( - features & cover.SUPPORT_OPEN == 0 - and features & cover.SUPPORT_CLOSE == 0 - ): - response["queryOnlyOpenClose"] = True + if ( + features & cover.SUPPORT_OPEN == 0 + and features & cover.SUPPORT_CLOSE == 0 + ): + response["queryOnlyOpenClose"] = True if self.state.attributes.get(ATTR_ASSUMED_STATE): response["commandOnlyOpenClose"] = True diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 8d7a675860c..365c118e99e 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -1,9 +1,11 @@ """Support for Google Cloud Pub/Sub.""" +from __future__ import annotations + import datetime import json import logging import os -from typing import Any, Dict +from typing import Any from google.cloud import pubsub_v1 import voluptuous as vol @@ -37,7 +39,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): +def setup(hass: HomeAssistant, yaml_config: dict[str, Any]): """Activate Google Pub/Sub component.""" config = yaml_config[DOMAIN] @@ -57,9 +59,7 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): service_principal_path ) - topic_path = publisher.topic_path( # pylint: disable=no-member - project_id, topic_name - ) + topic_path = publisher.topic_path(project_id, topic_name) encoder = DateTimeJSONEncoder() diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 098c6d2d59c..11bfb871a1b 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -5,7 +5,7 @@ import logging import googlemaps import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -18,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -180,7 +179,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) -class GoogleTravelTimeSensor(Entity): +class GoogleTravelTimeSensor(SensorEntity): """Representation of a Google travel time sensor.""" def __init__(self, hass, name, api_key, origin, destination, options): @@ -230,7 +229,7 @@ class GoogleTravelTimeSensor(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._matrix is None: return None diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 9dfa26fab75..28ec5df7486 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -14,7 +14,6 @@ from homeassistant.const import ( TIME_DAYS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) @@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class GoogleWifiSensor(Entity): +class GoogleWifiSensor(SensorEntity): """Representation of a Google Wifi sensor.""" def __init__(self, api, name, variable): @@ -176,9 +175,10 @@ class GoogleWifiAPI: sensor_value = "Online" else: sensor_value = "Offline" - elif attr_key == ATTR_LOCAL_IP: - if not self.raw_data["wan"]["online"]: - sensor_value = STATE_UNKNOWN + elif ( + attr_key == ATTR_LOCAL_IP and not self.raw_data["wan"]["online"] + ): + sensor_value = STATE_UNKNOWN self.data[attr_key] = sensor_value except KeyError: diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 2fa227f0953..5680eb75500 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -227,9 +227,8 @@ class GPMDP(MediaPlayerEntity): return while True: msg = json.loads(websocket.recv()) - if "requestID" in msg: - if msg["requestID"] == self._request_id: - return msg + if "requestID" in msg and msg["requestID"] == self._request_id: + return msg except ( ConnectionRefusedError, ConnectionResetError, diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index ea238269e59..2f97f62337c 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -5,7 +5,7 @@ import socket from gps3.agps3threaded import AGPS3mechanism import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_PORT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([GpsdSensor(hass, name, host, port)]) -class GpsdSensor(Entity): +class GpsdSensor(SensorEntity): """Representation of a GPS receiver available via GPSD.""" def __init__(self, hass, name, host, port): @@ -94,7 +93,7 @@ class GpsdSensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the GPS.""" return { ATTR_LATITUDE: self.agps_thread.data_stream.lat, diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 6999f26d752..25701e8c2e7 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -79,7 +79,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): return self._battery @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific attributes.""" return self._attributes diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index 4fa3678043e..fe459ca3164 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k." + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/translations/id.json b/homeassistant/components/gpslogger/translations/id.json new file mode 100644 index 00000000000..3be2d91f1f3 --- /dev/null +++ b/homeassistant/components/gpslogger/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di GPSLogger.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan GPSLogger Webhook?", + "title": "Siapkan GPSLogger Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/ko.json b/homeassistant/components/gpslogger/translations/ko.json index e73d72c06b7..3d32cb44736 100644 --- a/homeassistant/components/gpslogger/translations/ko.json +++ b/homeassistant/components/gpslogger/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 GPSLogger\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 327e8293be7..9405b576b4d 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to a Graphite installation.""" +from contextlib import suppress import logging import queue import socket @@ -11,6 +12,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_PREFIX, + CONF_PROTOCOL, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, @@ -20,8 +22,11 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +PROTOCOL_TCP = "tcp" +PROTOCOL_UDP = "udp" DEFAULT_HOST = "localhost" DEFAULT_PORT = 2003 +DEFAULT_PROTOCOL = PROTOCOL_TCP DEFAULT_PREFIX = "ha" DOMAIN = "graphite" @@ -31,6 +36,9 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.Any( + PROTOCOL_TCP, PROTOCOL_UDP + ), vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, } ) @@ -45,29 +53,34 @@ def setup(hass, config): host = conf.get(CONF_HOST) prefix = conf.get(CONF_PREFIX) port = conf.get(CONF_PORT) + protocol = conf.get(CONF_PROTOCOL) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - sock.connect((host, port)) - sock.shutdown(2) - _LOGGER.debug("Connection to Graphite possible") - except OSError: - _LOGGER.error("Not able to connect to Graphite") - return False + if protocol == PROTOCOL_TCP: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + sock.shutdown(2) + _LOGGER.debug("Connection to Graphite possible") + except OSError: + _LOGGER.error("Not able to connect to Graphite") + return False + else: + _LOGGER.debug("No connection check for UDP possible") - GraphiteFeeder(hass, host, port, prefix) + GraphiteFeeder(hass, host, port, protocol, prefix) return True class GraphiteFeeder(threading.Thread): """Feed data to Graphite.""" - def __init__(self, hass, host, port, prefix): + def __init__(self, hass, host, port, protocol, prefix): """Initialize the feeder.""" super().__init__(daemon=True) self._hass = hass self._host = host self._port = port + self._protocol = protocol # rstrip any trailing dots in case they think they need it self._prefix = prefix.rstrip(".") self._queue = queue.Queue() @@ -100,21 +113,23 @@ class GraphiteFeeder(threading.Thread): def _send_to_graphite(self, data): """Send data to Graphite.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(10) - sock.connect((self._host, self._port)) - sock.sendall(data.encode("ascii")) - sock.send(b"\n") - sock.close() + if self._protocol == PROTOCOL_TCP: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(10) + sock.connect((self._host, self._port)) + sock.sendall(data.encode("ascii")) + sock.send(b"\n") + sock.close() + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.sendto(data.encode("ascii") + b"\n", (self._host, self._port)) def _report_attributes(self, entity_id, new_state): """Report the attributes.""" now = time.time() things = dict(new_state.attributes) - try: + with suppress(ValueError): things["state"] = state.state_as_number(new_state) - except ValueError: - pass lines = [ "%s.%s.%s %f %i" % (self._prefix, entity_id, key.replace(" ", "_"), value, now) diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 3fbf4a21fb3..af523f385aa 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -1,7 +1,8 @@ """Helper and wrapper classes for Gree module.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import List from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery @@ -86,7 +87,7 @@ class DeviceHelper: return device @staticmethod - async def find_devices() -> List[DeviceInfo]: + async def find_devices() -> list[DeviceInfo]: """Gather a list of device infos from the local network.""" return await Discovery.search_devices() diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 8d0170fbe50..a5ef39be071 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -1,6 +1,7 @@ """Support for interface with a Gree climate systems.""" +from __future__ import annotations + import logging -from typing import List from greeclimate.device import ( FanSpeed, @@ -234,7 +235,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the HVAC modes support by the device.""" modes = [*HVAC_MODES_REVERSE] modes.append(HVAC_MODE_OFF) @@ -282,7 +283,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def preset_modes(self) -> List[str]: + def preset_modes(self) -> list[str]: """Return the preset modes support by the device.""" return PRESET_MODES @@ -302,7 +303,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def fan_modes(self) -> List[str]: + def fan_modes(self) -> list[str]: """Return the fan modes support by the device.""" return [*FAN_MODES_REVERSE] @@ -342,7 +343,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def swing_modes(self) -> List[str]: + def swing_modes(self) -> list[str]: """Return the swing modes currently supported for this device.""" return SWING_MODES diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index fa1f1550e83..12c94ddec61 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -1,5 +1,5 @@ """Support for interface with a Gree climate systems.""" -from typing import Optional +from __future__ import annotations from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -38,7 +38,7 @@ class GreeSwitchEntity(CoordinatorEntity, SwitchEntity): return f"{self._mac}-panel-light" @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon for the device.""" return "mdi:lightbulb" diff --git a/homeassistant/components/gree/translations/de.json b/homeassistant/components/gree/translations/de.json index 96ed09a974f..86bc8e36730 100644 --- a/homeassistant/components/gree/translations/de.json +++ b/homeassistant/components/gree/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/gree/translations/hu.json b/homeassistant/components/gree/translations/hu.json new file mode 100644 index 00000000000..6c61530acbe --- /dev/null +++ b/homeassistant/components/gree/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/id.json b/homeassistant/components/gree/translations/id.json new file mode 100644 index 00000000000..223836a8b40 --- /dev/null +++ b/homeassistant/components/gree/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/ko.json b/homeassistant/components/gree/translations/ko.json index 7011a61f757..e5ae04d6e5c 100644 --- a/homeassistant/components/gree/translations/ko.json +++ b/homeassistant/components/gree/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index f026bdfe3a4..4e792bf56e4 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,4 +1,5 @@ """Support for the sensors in a GreenEye Monitor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_NAME, CONF_SENSOR_TYPE, @@ -9,7 +10,6 @@ from homeassistant.const import ( TIME_SECONDS, VOLT, ) -from homeassistant.helpers.entity import Entity from . import ( CONF_COUNTED_QUANTITY, @@ -85,7 +85,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class GEMSensor(Entity): +class GEMSensor(SensorEntity): """Base class for GreenEye Monitor sensors.""" def __init__(self, monitor_serial_number, name, sensor_type, number): @@ -175,7 +175,7 @@ class CurrentSensor(GEMSensor): return self._sensor.watts @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return total wattseconds in the state dictionary.""" if not self._sensor: return None @@ -242,7 +242,7 @@ class PulseCounter(GEMSensor): return f"{self._counted_quantity}/{self._time_unit}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return total pulses in the data dictionary.""" if not self._sensor: return None diff --git a/homeassistant/components/griddy/__init__.py b/homeassistant/components/griddy/__init__.py deleted file mode 100644 index fb5079b00f8..00000000000 --- a/homeassistant/components/griddy/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -"""The Griddy Power integration.""" -import asyncio -from datetime import timedelta -import logging - -from griddypower.async_api import LOAD_ZONES, AsyncGriddy -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import CONF_LOADZONE, DOMAIN, UPDATE_INTERVAL - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_LOADZONE): vol.In(LOAD_ZONES)})}, - extra=vol.ALLOW_EXTRA, -) - -PLATFORMS = ["sensor"] - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Griddy Power component.""" - - hass.data.setdefault(DOMAIN, {}) - conf = config.get(DOMAIN) - - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_LOADZONE: conf.get(CONF_LOADZONE)}, - ) - ) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Griddy Power from a config entry.""" - - entry_data = entry.data - - async_griddy = AsyncGriddy( - aiohttp_client.async_get_clientsession(hass), - settlement_point=entry_data[CONF_LOADZONE], - ) - - async def async_update_data(): - """Fetch data from API endpoint.""" - return await async_griddy.async_getnow() - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="Griddy getnow", - update_method=async_update_data, - update_interval=timedelta(seconds=UPDATE_INTERVAL), - ) - - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady - - hass.data[DOMAIN][entry.entry_id] = coordinator - - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - - 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 - ] - ) - ) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/griddy/config_flow.py b/homeassistant/components/griddy/config_flow.py deleted file mode 100644 index 675e48cc999..00000000000 --- a/homeassistant/components/griddy/config_flow.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Config flow for Griddy Power integration.""" -import asyncio -import logging - -from aiohttp import ClientError -from griddypower.async_api import LOAD_ZONES, AsyncGriddy -import voluptuous as vol - -from homeassistant import config_entries, core, exceptions -from homeassistant.helpers import aiohttp_client - -from .const import CONF_LOADZONE -from .const import DOMAIN # pylint:disable=unused-import - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema({vol.Required(CONF_LOADZONE): vol.In(LOAD_ZONES)}) - - -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. - - Data has the keys from DATA_SCHEMA with values provided by the user. - """ - client_session = aiohttp_client.async_get_clientsession(hass) - - try: - await AsyncGriddy( - client_session, settlement_point=data[CONF_LOADZONE] - ).async_getnow() - except (asyncio.TimeoutError, ClientError) as err: - raise CannotConnect from err - - # Return info that you want to store in the config entry. - return {"title": f"Load Zone {data[CONF_LOADZONE]}"} - - -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Griddy Power.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - errors = {} - info = None - if user_input is not None: - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if "base" not in errors: - await self.async_set_unique_id(user_input[CONF_LOADZONE]) - 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=DATA_SCHEMA, errors=errors - ) - - async def async_step_import(self, user_input): - """Handle import.""" - await self.async_set_unique_id(user_input[CONF_LOADZONE]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/griddy/const.py b/homeassistant/components/griddy/const.py deleted file mode 100644 index 034567a806e..00000000000 --- a/homeassistant/components/griddy/const.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Constants for the Griddy Power integration.""" - -DOMAIN = "griddy" - -UPDATE_INTERVAL = 90 - -CONF_LOADZONE = "loadzone" diff --git a/homeassistant/components/griddy/manifest.json b/homeassistant/components/griddy/manifest.json deleted file mode 100644 index 1e31b1b7aa8..00000000000 --- a/homeassistant/components/griddy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "griddy", - "name": "Griddy Power", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/griddy", - "requirements": ["griddypower==0.1.0"], - "codeowners": ["@bdraco"] -} diff --git a/homeassistant/components/griddy/sensor.py b/homeassistant/components/griddy/sensor.py deleted file mode 100644 index f8a900d92be..00000000000 --- a/homeassistant/components/griddy/sensor.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Support for August sensors.""" -from homeassistant.const import CURRENCY_CENT, ENERGY_KILO_WATT_HOUR -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import CONF_LOADZONE, DOMAIN - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the August sensors.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - - settlement_point = config_entry.data[CONF_LOADZONE] - - async_add_entities([GriddyPriceSensor(settlement_point, coordinator)], True) - - -class GriddyPriceSensor(CoordinatorEntity): - """Representation of an August sensor.""" - - def __init__(self, settlement_point, coordinator): - """Initialize the sensor.""" - super().__init__(coordinator) - self._settlement_point = settlement_point - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return f"{CURRENCY_CENT}/{ENERGY_KILO_WATT_HOUR}" - - @property - def name(self): - """Device Name.""" - return f"{self._settlement_point} Price Now" - - @property - def icon(self): - """Device Ice.""" - return "mdi:currency-usd" - - @property - def unique_id(self): - """Device Uniqueid.""" - return f"{self._settlement_point}_price_now" - - @property - def state(self): - """Get the current price.""" - return round(float(self.coordinator.data.now.price_cents_kwh), 4) diff --git a/homeassistant/components/griddy/strings.json b/homeassistant/components/griddy/strings.json deleted file mode 100644 index 99bd8946c34..00000000000 --- a/homeassistant/components/griddy/strings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "description": "Your Load Zone is in your Griddy account under \u201cAccount > Meter > Load Zone.\u201d", - "data": { "loadzone": "Load Zone (Settlement Point)" }, - "title": "Setup your Griddy Load Zone" - } - }, - "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } - } -} diff --git a/homeassistant/components/griddy/translations/ca.json b/homeassistant/components/griddy/translations/ca.json deleted file mode 100644 index 33aca3cd302..00000000000 --- a/homeassistant/components/griddy/translations/ca.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "loadzone": "Zona de c\u00e0rrega (Load Zone)" - }, - "description": "La teva zona de c\u00e0rrega (Load Zone) est\u00e0 al teu compte de Griddy v\u00e9s a \"Account > Meter > Load Zone\".", - "title": "Configuraci\u00f3 de la zona de c\u00e0rrega (Load Zone) de Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/cs.json b/homeassistant/components/griddy/translations/cs.json deleted file mode 100644 index fa5918fa5da..00000000000 --- a/homeassistant/components/griddy/translations/cs.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" - }, - "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/de.json b/homeassistant/components/griddy/translations/de.json deleted file mode 100644 index 4a6c477059c..00000000000 --- a/homeassistant/components/griddy/translations/de.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standort ist bereits konfiguriert" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "loadzone": "Ladezone (Abwicklungspunkt)" - }, - "description": "Ihre Ladezone befindet sich in Ihrem Griddy-Konto unter \"Konto > Messger\u00e4t > Ladezone\".", - "title": "Richten Sie Ihre Griddy Ladezone ein" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/en.json b/homeassistant/components/griddy/translations/en.json deleted file mode 100644 index 2a82421dd7c..00000000000 --- a/homeassistant/components/griddy/translations/en.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Location is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "loadzone": "Load Zone (Settlement Point)" - }, - "description": "Your Load Zone is in your Griddy account under \u201cAccount > Meter > Load Zone.\u201d", - "title": "Setup your Griddy Load Zone" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/es-419.json b/homeassistant/components/griddy/translations/es-419.json deleted file mode 100644 index 652c8484b4e..00000000000 --- a/homeassistant/components/griddy/translations/es-419.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta zona de carga ya est\u00e1 configurada" - }, - "error": { - "cannot_connect": "No se pudo conectar, intente nuevamente", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "loadzone": "Zona de carga (punto de asentamiento)" - }, - "description": "Su zona de carga est\u00e1 en su cuenta de Griddy en \"Cuenta > Medidor > Zona de carga\".", - "title": "Configura tu zona de carga Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/es.json b/homeassistant/components/griddy/translations/es.json deleted file mode 100644 index a3727721b2d..00000000000 --- a/homeassistant/components/griddy/translations/es.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta Zona de Carga ya est\u00e1 configurada" - }, - "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "loadzone": "Zona de Carga (Punto del Asentamiento)" - }, - "description": "Tu Zona de Carga est\u00e1 en tu cuenta de Griddy en \"Account > Meter > Load Zone\"", - "title": "Configurar tu Zona de Carga de Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/et.json b/homeassistant/components/griddy/translations/et.json deleted file mode 100644 index 82d2232b04e..00000000000 --- a/homeassistant/components/griddy/translations/et.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "See Load Zone on juba m\u00e4\u00e4ratud" - }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "unknown": "Tundmatu viga" - }, - "step": { - "user": { - "data": { - "loadzone": "Load Zone (arvelduspunkt)" - }, - "description": "Load Zone asub Griddy konto valikutes \u201cAccount > Meter > Load Zone.\u201d", - "title": "Seadista oma Griddy Load Zone" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/fr.json b/homeassistant/components/griddy/translations/fr.json deleted file mode 100644 index c2fd4d8d627..00000000000 --- a/homeassistant/components/griddy/translations/fr.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cette zone de chargement est d\u00e9j\u00e0 configur\u00e9e" - }, - "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "loadzone": "Zone de charge (point d'\u00e9tablissement)" - }, - "description": "Votre zone de charge se trouve dans votre compte Griddy sous \"Compte > Compteur > Zone de charge\".", - "title": "Configurez votre zone de charge Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/it.json b/homeassistant/components/griddy/translations/it.json deleted file mode 100644 index 40fa69b1229..00000000000 --- a/homeassistant/components/griddy/translations/it.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" - }, - "error": { - "cannot_connect": "Impossibile connettersi", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "loadzone": "Zona di Carico (Punto di insediamento)" - }, - "description": "La tua Zona di Carico si trova nel tuo account Griddy in \"Account > Meter > Load zone\".", - "title": "Configurazione della Zona di Carico Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/ko.json b/homeassistant/components/griddy/translations/ko.json deleted file mode 100644 index df9178fab93..00000000000 --- a/homeassistant/components/griddy/translations/ko.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "loadzone": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed (\uc815\uc0b0\uc810)" - }, - "description": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 Griddy \uacc4\uc815\uc758 \"Account > Meter > Load Zone\"\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "Griddy \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed \uc124\uc815\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/lb.json b/homeassistant/components/griddy/translations/lb.json deleted file mode 100644 index 84511186f88..00000000000 --- a/homeassistant/components/griddy/translations/lb.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standuert ass scho konfigur\u00e9iert" - }, - "error": { - "cannot_connect": "Feeler beim verbannen", - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "loadzone": "Lued Zone (Punkt vum R\u00e9glement)" - }, - "description": "Deng Lued Zon ass an dengem Griddy Kont enner \"Account > Meter > Load Zone.\"", - "title": "Griddy Lued Zon ariichten" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/nl.json b/homeassistant/components/griddy/translations/nl.json deleted file mode 100644 index bd97b9ccf7c..00000000000 --- a/homeassistant/components/griddy/translations/nl.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Deze laadzone is al geconfigureerd" - }, - "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "loadzone": "Laadzone (vestigingspunt)" - }, - "description": "Uw Load Zone staat op uw Griddy account onder \"Account > Meter > Load Zone\".", - "title": "Stel uw Griddy Load Zone in" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/no.json b/homeassistant/components/griddy/translations/no.json deleted file mode 100644 index 7f01fa198a3..00000000000 --- a/homeassistant/components/griddy/translations/no.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Plasseringen er allerede konfigurert" - }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "loadzone": "Load Zone (settlingspunkt)" - }, - "description": "Din Load Zone er p\u00e5 din Griddy-konto under \"Konto > M\u00e5ler > Lastesone.\"", - "title": "Sett opp din Griddy Load Zone" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/pl.json b/homeassistant/components/griddy/translations/pl.json deleted file mode 100644 index 035521336f6..00000000000 --- a/homeassistant/components/griddy/translations/pl.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "loadzone": "Strefa obci\u0105\u017cenia (punkt rozliczenia)" - }, - "description": "Twoja strefa obci\u0105\u017cenia znajduje si\u0119 na twoim koncie Griddy w sekcji \"Konto > Licznik > Strefa obci\u0105\u017cenia\".", - "title": "Konfiguracja strefy obci\u0105\u017cenia Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/pt.json b/homeassistant/components/griddy/translations/pt.json deleted file mode 100644 index 9b067d35f89..00000000000 --- a/homeassistant/components/griddy/translations/pt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" - }, - "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "unknown": "Erro inesperado" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/ru.json b/homeassistant/components/griddy/translations/ru.json deleted file mode 100644 index 3483953fce1..00000000000 --- a/homeassistant/components/griddy/translations/ru.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "loadzone": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430)" - }, - "description": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Griddy \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 Account > Meter > Load Zone.", - "title": "Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/sl.json b/homeassistant/components/griddy/translations/sl.json deleted file mode 100644 index 8df85c6dc67..00000000000 --- a/homeassistant/components/griddy/translations/sl.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ta obremenitvena cona je \u017ee konfigurirana" - }, - "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova", - "unknown": "Nepri\u010dakovana napaka" - }, - "step": { - "user": { - "data": { - "loadzone": "Obremenitvena cona (poselitvena to\u010dka)" - }, - "description": "Va\u0161a obremenitvena cona je v va\u0161em ra\u010dunu Griddy pod \"Ra\u010dun > Merilnik > Nalo\u017ei cono.\"", - "title": "Nastavite svojo Griddy Load Cono" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/sv.json b/homeassistant/components/griddy/translations/sv.json deleted file mode 100644 index e9ddacf2714..00000000000 --- a/homeassistant/components/griddy/translations/sv.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", - "unknown": "Ov\u00e4ntat fel" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/tr.json b/homeassistant/components/griddy/translations/tr.json deleted file mode 100644 index 26e0fa73065..00000000000 --- a/homeassistant/components/griddy/translations/tr.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, - "error": { - "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131, l\u00fctfen tekrar deneyin", - "unknown": "Beklenmeyen hata" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/uk.json b/homeassistant/components/griddy/translations/uk.json deleted file mode 100644 index e366f0e8b24..00000000000 --- a/homeassistant/components/griddy/translations/uk.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." - }, - "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "loadzone": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f (\u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0430 \u0442\u043e\u0447\u043a\u0430)" - }, - "description": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043f\u0440\u043e\u0444\u0456\u043b\u0456 Griddy \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 Account > Meter > Load Zone.", - "title": "Griddy" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/zh-Hant.json b/homeassistant/components/griddy/translations/zh-Hant.json deleted file mode 100644 index 4918c1e818e..00000000000 --- a/homeassistant/components/griddy/translations/zh-Hant.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "loadzone": "\u8ca0\u8f09\u5340\u57df\uff08\u5c45\u4f4f\u9ede\uff09" - }, - "description": "\u8ca0\u8f09\u5340\u57df\u986f\u793a\u65bc Griddy \u5e33\u865f\uff0c\u4f4d\u65bc \u201cAccount > Meter > Load Zone\u201d\u3002", - "title": "\u8a2d\u5b9a Griddy \u8ca0\u8f09\u5340\u57df" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index f185601ce87..5af53768bc0 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,9 +1,11 @@ """Provide the functionality to group entities.""" +from __future__ import annotations + from abc import abstractmethod import asyncio from contextvars import ContextVar import logging -from typing import Any, Dict, Iterable, List, Optional, Set, cast +from typing import Any, Iterable, List, cast import voluptuous as vol @@ -23,7 +25,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import CoreState, callback, split_entity_id +from homeassistant.core import CoreState, HomeAssistant, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent @@ -32,7 +34,6 @@ from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) from homeassistant.helpers.reload import async_reload_integration_platforms -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -91,16 +92,16 @@ CONFIG_SCHEMA = vol.Schema( class GroupIntegrationRegistry: """Class to hold a registry of integrations.""" - on_off_mapping: Dict[str, str] = {STATE_ON: STATE_OFF} - off_on_mapping: Dict[str, str] = {STATE_OFF: STATE_ON} - on_states_by_domain: Dict[str, Set] = {} - exclude_domains: Set = set() + on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} + off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} + on_states_by_domain: dict[str, set] = {} + exclude_domains: set = set() def exclude_domain(self) -> None: """Exclude the current domain.""" self.exclude_domains.add(current_domain.get()) - def on_off_states(self, on_states: Set, off_state: str) -> None: + def on_off_states(self, on_states: set, off_state: str) -> None: """Register on and off states for the current domain.""" for on_state in on_states: if on_state not in self.on_off_mapping: @@ -128,12 +129,12 @@ def is_on(hass, entity_id): @bind_hass -def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> List[str]: +def expand_entity_ids(hass: HomeAssistant, entity_ids: Iterable[Any]) -> list[str]: """Return entity_ids with group entity ids replaced by their members. Async friendly. """ - found_ids: List[str] = [] + found_ids: list[str] = [] for entity_id in entity_ids: if not isinstance(entity_id, str) or entity_id in ( ENTITY_MATCH_NONE, @@ -171,8 +172,8 @@ def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> Lis @bind_hass def get_entity_ids( - hass: HomeAssistantType, entity_id: str, domain_filter: Optional[str] = None -) -> List[str]: + hass: HomeAssistant, entity_id: str, domain_filter: str | None = None +) -> list[str]: """Get members of this group. Async friendly. @@ -192,7 +193,7 @@ def get_entity_ids( @bind_hass -def groups_with_entity(hass: HomeAssistantType, entity_id: str) -> List[str]: +def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: """Get all groups that contain this entity. Async friendly. @@ -395,7 +396,6 @@ class GroupEntity(Entity): async def async_added_to_hass(self) -> None: """Register listeners.""" - assert self.hass is not None async def _update_at_start(_): await self.async_update() @@ -405,8 +405,6 @@ class GroupEntity(Entity): async def async_defer_or_update_ha_state(self) -> None: """Only update once at start.""" - assert self.hass is not None - if self.hass.state != CoreState.running: return @@ -548,7 +546,7 @@ class Group(Entity): self._icon = value @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes for the group.""" data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order} if not self.user_defined: diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index b52546c48d7..5e8d18b28e2 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -1,5 +1,5 @@ """This platform allows several cover to be grouped into one cover.""" -from typing import Dict, Optional, Set +from __future__ import annotations import voluptuous as vol @@ -76,18 +76,18 @@ class CoverGroup(GroupEntity, CoverEntity): self._is_closed = False self._is_closing = False self._is_opening = False - self._cover_position: Optional[int] = 100 + self._cover_position: int | None = 100 self._tilt_position = None self._supported_features = 0 self._assumed_state = True self._entities = entities - self._covers: Dict[str, Set[str]] = { + self._covers: dict[str, set[str]] = { KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set(), } - self._tilts: Dict[str, Set[str]] = { + self._tilts: dict[str, set[str]] = { KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set(), @@ -102,7 +102,7 @@ class CoverGroup(GroupEntity, CoverEntity): async def async_update_supported_features( self, entity_id: str, - new_state: Optional[State], + new_state: State | None, update_state: bool = True, ) -> None: """Update dictionaries with supported features.""" @@ -155,7 +155,6 @@ class CoverGroup(GroupEntity, CoverEntity): await self.async_update_supported_features( entity_id, new_state, update_state=False ) - assert self.hass is not None self.async_on_remove( async_track_state_change_event( self.hass, self._entities, self._update_supported_features_event @@ -198,7 +197,7 @@ class CoverGroup(GroupEntity, CoverEntity): return self._is_closing @property - def current_cover_position(self) -> Optional[int]: + def current_cover_position(self) -> int | None: """Return current position for all covers.""" return self._cover_position @@ -208,7 +207,7 @@ class CoverGroup(GroupEntity, CoverEntity): return self._tilt_position @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes for the cover group.""" return {ATTR_ENTITY_ID: self._entities} diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 00b7321076f..b45dd1ec5e3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -1,8 +1,10 @@ """This platform allows several lights to be grouped into one light.""" +from __future__ import annotations + import asyncio from collections import Counter import itertools -from typing import Any, Callable, Iterator, List, Optional, Tuple, cast +from typing import Any, Callable, Iterator, cast import voluptuous as vol @@ -35,10 +37,10 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CoreState, State +from homeassistant.core import CoreState, HomeAssistant, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import color as color_util from . import GroupEntity @@ -67,7 +69,7 @@ SUPPORT_GROUP_LIGHT = ( 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: """Initialize light.group platform.""" async_add_entities( @@ -78,21 +80,21 @@ async def async_setup_platform( class LightGroup(GroupEntity, light.LightEntity): """Representation of a light group.""" - def __init__(self, name: str, entity_ids: List[str]) -> None: + def __init__(self, name: str, entity_ids: list[str]) -> None: """Initialize a light group.""" self._name = name self._entity_ids = entity_ids self._is_on = False self._available = False self._icon = "mdi:lightbulb-group" - self._brightness: Optional[int] = None - self._hs_color: Optional[Tuple[float, float]] = None - self._color_temp: Optional[int] = None - self._min_mireds: Optional[int] = 154 - self._max_mireds: Optional[int] = 500 - self._white_value: Optional[int] = None - self._effect_list: Optional[List[str]] = None - self._effect: Optional[str] = None + self._brightness: int | None = None + self._hs_color: tuple[float, float] | None = None + self._color_temp: int | None = None + self._min_mireds: int = 154 + self._max_mireds: int = 500 + self._white_value: int | None = None + self._effect_list: list[str] | None = None + self._effect: str | None = None self._supported_features: int = 0 async def async_added_to_hass(self) -> None: @@ -103,7 +105,6 @@ class LightGroup(GroupEntity, light.LightEntity): self.async_set_context(event.context) await self.async_defer_or_update_ha_state() - assert self.hass self.async_on_remove( async_track_state_change_event( self.hass, self._entity_ids, async_state_changed_listener @@ -137,42 +138,42 @@ class LightGroup(GroupEntity, light.LightEntity): return self._icon @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of this light group between 0..255.""" return self._brightness @property - def hs_color(self) -> Optional[Tuple[float, float]]: + def hs_color(self) -> tuple[float, float] | None: """Return the HS color value [float, float].""" return self._hs_color @property - def color_temp(self) -> Optional[int]: + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" return self._color_temp @property - def min_mireds(self) -> Optional[int]: + def min_mireds(self) -> int: """Return the coldest color_temp that this light group supports.""" return self._min_mireds @property - def max_mireds(self) -> Optional[int]: + def max_mireds(self) -> int: """Return the warmest color_temp that this light group supports.""" return self._max_mireds @property - def white_value(self) -> Optional[int]: + def white_value(self) -> int | None: """Return the white value of this light group between 0..255.""" return self._white_value @property - def effect_list(self) -> Optional[List[str]]: + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return self._effect_list @property - def effect(self) -> Optional[str]: + def effect(self) -> str | None: """Return the current effect.""" return self._effect @@ -187,7 +188,7 @@ class LightGroup(GroupEntity, light.LightEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes for the light group.""" return {ATTR_ENTITY_ID: self._entity_ids} @@ -289,7 +290,7 @@ class LightGroup(GroupEntity, light.LightEntity): async def async_update(self): """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: List[State] = list(filter(None, all_states)) + states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._is_on = len(on_states) > 0 @@ -332,7 +333,7 @@ class LightGroup(GroupEntity, light.LightEntity): self._supported_features &= SUPPORT_GROUP_LIGHT -def _find_state_attributes(states: List[State], key: str) -> Iterator[Any]: +def _find_state_attributes(states: list[State], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: value = state.attributes.get(key) @@ -351,9 +352,9 @@ def _mean_tuple(*args): def _reduce_attribute( - states: List[State], + states: list[State], key: str, - default: Optional[Any] = None, + default: Any | None = None, reduce: Callable[..., Any] = _mean_int, ) -> Any: """Find the first attribute matching key from states. diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index adeb0cfee0a..ea21c147b9b 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -1,19 +1,20 @@ """Module that groups code required to handle state restore for component.""" -from typing import Any, Dict, Iterable, Optional +from __future__ import annotations -from homeassistant.core import Context, State +from typing import Any, Iterable + +from homeassistant.core import Context, HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state -from homeassistant.helpers.typing import HomeAssistantType from . import get_entity_ids async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" states_copy = [] diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json index 9a38f0f2de3..553cbce0550 100644 --- a/homeassistant/components/group/translations/id.json +++ b/homeassistant/components/group/translations/id.json @@ -2,14 +2,14 @@ "state": { "_": { "closed": "Tertutup", - "home": "Rumah", + "home": "Di Rumah", "locked": "Terkunci", "not_home": "Keluar", - "off": "Off", - "ok": "OK", - "on": "On", + "off": "Mati", + "ok": "Oke", + "on": "Nyala", "open": "Terbuka", - "problem": "Masalah", + "problem": "Bermasalah", "unlocked": "Terbuka" } }, diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index e6ed422db0f..86b88872a8a 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -7,7 +7,7 @@ import re import growattServer import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -28,7 +28,6 @@ from homeassistant.const import ( VOLT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -399,7 +398,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = STORAGE_SENSOR_TYPES else: _LOGGER.debug( - "Device type %s was found but is not supported right now.", + "Device type %s was found but is not supported right now", device["deviceType"], ) @@ -416,7 +415,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class GrowattInverter(Entity): +class GrowattInverter(SensorEntity): """Representation of a Growatt Sensor.""" def __init__(self, probe, name, sensor, unique_id): diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index ea211ccd748..c927b04de25 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -82,7 +82,7 @@ class GstreamerDevice(MediaPlayerEntity): def play_media(self, media_type, media_id, **kwargs): """Play media.""" if media_type != MEDIA_TYPE_MUSIC: - _LOGGER.error("invalid media type") + _LOGGER.error("Invalid media type") return self._player.queue(media_id) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index d21ab67f053..46a31f464a1 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -1,15 +1,17 @@ """Support for GTFS (Google/General Transport Format Schema).""" +from __future__ import annotations + import datetime import logging import os import threading -from typing import Any, Callable, Optional +from typing import Any, Callable import pygtfs from sqlalchemy.sql import text import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_NAME, @@ -18,7 +20,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ( ConfigType, DiscoveryInfoType, @@ -484,7 +485,7 @@ def setup_platform( hass: HomeAssistantType, config: ConfigType, add_entities: Callable[[list], None], - discovery_info: Optional[DiscoveryInfoType] = None, + discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GTFS sensor.""" gtfs_dir = hass.config.path(DEFAULT_PATH) @@ -517,13 +518,13 @@ def setup_platform( ) -class GTFSDepartureSensor(Entity): +class GTFSDepartureSensor(SensorEntity): """Implementation of a GTFS departure sensor.""" def __init__( self, gtfs: Any, - name: Optional[Any], + name: Any | None, origin: Any, destination: Any, offset: cv.time_period, @@ -540,7 +541,7 @@ class GTFSDepartureSensor(Entity): self._available = False self._icon = ICON self._name = "" - self._state: Optional[str] = None + self._state: str | None = None self._attributes = {} self._agency = None @@ -559,7 +560,7 @@ class GTFSDepartureSensor(Entity): return self._name @property - def state(self) -> Optional[str]: # type: ignore + def state(self) -> str | None: # type: ignore """Return the state of the sensor.""" return self._state @@ -569,7 +570,7 @@ class GTFSDepartureSensor(Entity): return self._available @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attributes @@ -811,7 +812,7 @@ class GTFSDepartureSensor(Entity): col: getattr(resource, col) for col in resource.__table__.columns.keys() } - def append_keys(self, resource: dict, prefix: Optional[str] = None) -> None: + def append_keys(self, resource: dict, prefix: str | None = None) -> None: """Properly format key val pairs to append to attributes.""" for attr, val in resource.items(): if val == "" or val is None or attr == "feed_id": diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index fdc82f14778..ebb5e71e1cb 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -1,6 +1,7 @@ """The Elexa Guardian integration.""" +from __future__ import annotations + import asyncio -from typing import Dict from aioguardian import Client @@ -104,9 +105,9 @@ 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 component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -117,8 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -246,7 +247,7 @@ class GuardianEntity(CoordinatorEntity): return self._device_info @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attrs @@ -314,7 +315,7 @@ class ValveControllerEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinators: Dict[str, DataUpdateCoordinator], + coordinators: dict[str, DataUpdateCoordinator], kind: str, name: str, device_class: str, diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index d8d0498304d..e8c736eabe5 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,5 +1,7 @@ """Binary sensors for the Elexa Guardian integration.""" -from typing import Callable, Dict, Optional +from __future__ import annotations + +from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -122,8 +124,8 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): coordinator: DataUpdateCoordinator, kind: str, name: str, - device_class: Optional[str], - icon: Optional[str], + device_class: str | None, + icon: str | None, ) -> None: """Initialize.""" super().__init__(entry, coordinator, kind, name, device_class, icon) @@ -155,11 +157,11 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): def __init__( self, entry: ConfigEntry, - coordinators: Dict[str, DataUpdateCoordinator], + coordinators: dict[str, DataUpdateCoordinator], kind: str, name: str, - device_class: Optional[str], - icon: Optional[str], + device_class: str | None, + icon: str | None, ) -> None: """Initialize.""" super().__init__(entry, coordinators, kind, name, device_class, icon) diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index a9286467afc..d6dcc0614f2 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, core from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import callback -from .const import CONF_UID, DOMAIN, LOGGER # pylint:disable=unused-import +from .const import CONF_UID, DOMAIN, LOGGER DATA_SCHEMA = vol.Schema( {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PORT, default=7777): int} diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 160246b2014..48807c9cfeb 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,6 +1,9 @@ """Sensors for the Elexa Guardian integration.""" -from typing import Callable, Dict, Optional +from __future__ import annotations +from typing import Callable + +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -108,7 +111,7 @@ async def async_setup_entry( async_add_entities(sensors) -class PairedSensorSensor(PairedSensorEntity): +class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Define a binary sensor related to a Guardian valve controller.""" def __init__( @@ -117,9 +120,9 @@ class PairedSensorSensor(PairedSensorEntity): coordinator: DataUpdateCoordinator, kind: str, name: str, - device_class: Optional[str], - icon: Optional[str], - unit: Optional[str], + device_class: str | None, + icon: str | None, + unit: str | None, ) -> None: """Initialize.""" super().__init__(entry, coordinator, kind, name, device_class, icon) @@ -151,18 +154,18 @@ class PairedSensorSensor(PairedSensorEntity): self._state = self.coordinator.data["temperature"] -class ValveControllerSensor(ValveControllerEntity): +class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Define a generic Guardian sensor.""" def __init__( self, entry: ConfigEntry, - coordinators: Dict[str, DataUpdateCoordinator], + coordinators: dict[str, DataUpdateCoordinator], kind: str, name: str, - device_class: Optional[str], - icon: Optional[str], - unit: Optional[str], + device_class: str | None, + icon: str | None, + unit: str | None, ) -> None: """Initialize.""" super().__init__(entry, coordinators, kind, name, device_class, icon) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 20a38ea5ce7..c574f283bdd 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,5 +1,7 @@ """Switches for the Elexa Guardian integration.""" -from typing import Callable, Dict +from __future__ import annotations + +from typing import Callable from aioguardian import Client from aioguardian.errors import GuardianError @@ -84,7 +86,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): self, entry: ConfigEntry, client: Client, - coordinators: Dict[str, DataUpdateCoordinator], + coordinators: dict[str, DataUpdateCoordinator], ): """Initialize.""" super().__init__( diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 563ede56155..bd43ce7672c 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -1,7 +1,17 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm", + "port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json new file mode 100644 index 00000000000..b5b75321037 --- /dev/null +++ b/homeassistant/components/guardian/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "port": "Port" + }, + "description": "Konfigurasikan perangkat Elexa Guardian lokal." + }, + "zeroconf_confirm": { + "description": "Ingin menyiapkan perangkat Guardian ini?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json index 959a847a7cf..a33cb9357a9 100644 --- a/homeassistant/components/guardian/translations/nl.json +++ b/homeassistant/components/guardian/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Dit Guardian-apparaat is al geconfigureerd.", - "already_in_progress": "De configuratie van het Guardian-apparaat is al bezig.", + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken" }, "step": { diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 64680a56bb3..159e760e223 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -46,7 +46,7 @@ INSTANCE_SCHEMA = vol.All( ), ) -has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name +has_unique_values = vol.Schema(vol.Unique()) # because we want a handy alias @@ -148,9 +148,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) data[config_entry.entry_id] = api - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): @@ -166,8 +166,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 6e3311ea9b5..051c15d1715 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -1,6 +1,7 @@ """Config flow for habitica integration.""" +from __future__ import annotations + import logging -from typing import Dict from aiohttp import ClientResponseError from habitipy.aio import HabitipyAsync @@ -10,7 +11,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_API_USER, DEFAULT_URL, DOMAIN # pylint: disable=unused-import +from .const import CONF_API_USER, DEFAULT_URL, DOMAIN DATA_SCHEMA = vol.Schema( { @@ -25,8 +26,8 @@ _LOGGER = logging.getLogger(__name__) async def validate_input( - hass: core.HomeAssistant, data: Dict[str, str] -) -> Dict[str, str]: + hass: core.HomeAssistant, data: dict[str, str] +) -> dict[str, str]: """Validate the user input allows us to connect.""" websession = async_get_clientsession(hass) diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 29e494d89ee..52748ddadad 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -5,8 +5,8 @@ import logging from aiohttp import ClientResponseError +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, HTTP_TOO_MANY_REQUESTS -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from .const import DOMAIN @@ -96,8 +96,8 @@ class HabitipyData: except ClientResponseError as error: if error.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( - "Sensor data update for %s has too many API requests." - " Skipping the update.", + "Sensor data update for %s has too many API requests;" + " Skipping the update", DOMAIN, ) else: @@ -113,8 +113,8 @@ class HabitipyData: except ClientResponseError as error: if error.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( - "Sensor data update for %s has too many API requests." - " Skipping the update.", + "Sensor data update for %s has too many API requests;" + " Skipping the update", DOMAIN, ) else: @@ -125,7 +125,7 @@ class HabitipyData: ) -class HabitipySensor(Entity): +class HabitipySensor(SensorEntity): """A generic Habitica sensor.""" def __init__(self, name, sensor_name, updater): @@ -165,7 +165,7 @@ class HabitipySensor(Entity): return self._sensor_type.unit -class HabitipyTaskSensor(Entity): +class HabitipyTaskSensor(SensorEntity): """A Habitica task sensor.""" def __init__(self, name, task_name, updater): @@ -200,7 +200,7 @@ class HabitipyTaskSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of all user tasks.""" if self._updater.tasks is not None: all_received_tasks = self._updater.tasks diff --git a/homeassistant/components/habitica/translations/bg.json b/homeassistant/components/habitica/translations/bg.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/habitica/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/hu.json b/homeassistant/components/habitica/translations/hu.json new file mode 100644 index 00000000000..4914a1bd27a --- /dev/null +++ b/homeassistant/components/habitica/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "api_user": "Habitica API felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja", + "url": "URL" + } + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/id.json b/homeassistant/components/habitica/translations/id.json new file mode 100644 index 00000000000..c7e4c549206 --- /dev/null +++ b/homeassistant/components/habitica/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_credentials": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "api_user": "ID pengguna API Habitica", + "name": "Ganti nama pengguna Habitica. Nama ini akan digunakan untuk panggilan layanan", + "url": "URL" + }, + "description": "Hubungkan profil Habitica Anda untuk memungkinkan pemantauan profil dan tugas pengguna Anda. Perhatikan bahwa api_id dan api_key harus diperoleh dari https://habitica.com/user/settings/api" + } + } + }, + "title": "Habitica" +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ko.json b/homeassistant/components/habitica/translations/ko.json index 3fd04a4477b..6b890a320df 100644 --- a/homeassistant/components/habitica/translations/ko.json +++ b/homeassistant/components/habitica/translations/ko.json @@ -8,8 +8,11 @@ "user": { "data": { "api_key": "API \ud0a4", + "api_user": "Habitica\uc758 API \uc0ac\uc6a9\uc790 ID", + "name": "Habitica\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uc744 \uc7ac\uc815\uc758\ud574\uc8fc\uc138\uc694. \uc11c\ube44\uc2a4 \ud638\ucd9c\uc5d0 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", "url": "URL \uc8fc\uc18c" - } + }, + "description": "\uc0ac\uc6a9\uc790\uc758 \ud504\ub85c\ud544 \ubc0f \uc791\uc5c5\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud560 \uc218 \uc788\ub3c4\ub85d \ud558\ub824\uba74 Habitica \ud504\ub85c\ud544\uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694.\n\ucc38\uace0\ub85c api_id \ubc0f api_key\ub294 https://habitica.com/user/settings/api \uc5d0\uc11c \uac00\uc838\uc640\uc57c \ud569\ub2c8\ub2e4." } } }, diff --git a/homeassistant/components/habitica/translations/nl.json b/homeassistant/components/habitica/translations/nl.json index 13a4fd6c729..817ffd8c616 100644 --- a/homeassistant/components/habitica/translations/nl.json +++ b/homeassistant/components/habitica/translations/nl.json @@ -8,8 +8,11 @@ "user": { "data": { "api_key": "API-sleutel", + "api_user": "Habitica's API-gebruikers-ID", + "name": "Vervanging voor de gebruikersnaam van Habitica. Wordt gebruikt voor serviceoproepen", "url": "URL" - } + }, + "description": "Verbind uw Habitica-profiel om het profiel en de taken van uw gebruiker te bewaken. Houd er rekening mee dat api_id en api_key van https://habitica.com/user/settings/api moeten worden gehaald" } } }, diff --git a/homeassistant/components/habitica/translations/pt.json b/homeassistant/components/habitica/translations/pt.json new file mode 100644 index 00000000000..034099e4828 --- /dev/null +++ b/homeassistant/components/habitica/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 56045f0eb1c..65e3c3923ad 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,5 +1,6 @@ """The Hangouts Bot.""" import asyncio +from contextlib import suppress import io import logging @@ -103,12 +104,10 @@ class HangoutsBot: self._conversation_intents[conv_id][intent_type] = data - try: + with suppress(ValueError): self._conversation_list.on_event.remove_observer( self._async_handle_conversation_event ) - except ValueError: - pass self._conversation_list.on_event.add_observer( self._async_handle_conversation_event ) @@ -221,7 +220,7 @@ class HangoutsBot: async def _on_disconnect(self): """Handle disconnecting.""" if self._connected: - _LOGGER.debug("Connection lost! Reconnect...") + _LOGGER.debug("Connection lost! Reconnect") await self.async_connect() else: dispatcher.async_dispatcher_send(self.hass, EVENT_HANGOUTS_DISCONNECTED) diff --git a/homeassistant/components/hangouts/strings.json b/homeassistant/components/hangouts/strings.json index bed46e823d9..0128363a1ab 100644 --- a/homeassistant/components/hangouts/strings.json +++ b/homeassistant/components/hangouts/strings.json @@ -7,7 +7,7 @@ "error": { "invalid_login": "Invalid Login, please try again.", "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", - "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone)." + "invalid_2fa_method": "Invalid 2FA Method (verify on Phone)." }, "step": { "user": { @@ -20,7 +20,7 @@ }, "2fa": { "data": { - "2fa": "2FA Pin" + "2fa": "2FA PIN" }, "title": "2-Factor-Authentication" } diff --git a/homeassistant/components/hangouts/translations/ca.json b/homeassistant/components/hangouts/translations/ca.json index 2d1d81bb08d..b4114312f3e 100644 --- a/homeassistant/components/hangouts/translations/ca.json +++ b/homeassistant/components/hangouts/translations/ca.json @@ -6,13 +6,13 @@ }, "error": { "invalid_2fa": "La verificaci\u00f3 en dos passos no \u00e9s v\u00e0lida, torna-ho a provar.", - "invalid_2fa_method": "El m\u00e8tode de verificaci\u00f3 en dos passos no \u00e9s v\u00e0lid (verifica-ho al m\u00f2bil).", + "invalid_2fa_method": "M\u00e8tode 2FA inv\u00e0lid (verifica-ho al m\u00f2bil).", "invalid_login": "L'inici de sessi\u00f3 no \u00e9s v\u00e0lid, torna-ho a provar." }, "step": { "2fa": { "data": { - "2fa": "Pin 2FA" + "2fa": "PIN 2FA" }, "description": "Buit", "title": "Verificaci\u00f3 en dos passos" diff --git a/homeassistant/components/hangouts/translations/en.json b/homeassistant/components/hangouts/translations/en.json index 5de8ac24970..b2d7076bd75 100644 --- a/homeassistant/components/hangouts/translations/en.json +++ b/homeassistant/components/hangouts/translations/en.json @@ -6,13 +6,13 @@ }, "error": { "invalid_2fa": "Invalid 2 Factor Authentication, please try again.", - "invalid_2fa_method": "Invalid 2FA Method (Verify on Phone).", + "invalid_2fa_method": "Invalid 2FA Method (verify on Phone).", "invalid_login": "Invalid Login, please try again." }, "step": { "2fa": { "data": { - "2fa": "2FA Pin" + "2fa": "2FA PIN" }, "title": "2-Factor-Authentication" }, diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json index 6bcc19d2043..7d6deb2ef53 100644 --- a/homeassistant/components/hangouts/translations/et.json +++ b/homeassistant/components/hangouts/translations/et.json @@ -6,13 +6,13 @@ }, "error": { "invalid_2fa": "Vale 2-teguriline autentimine, proovi uuesti.", - "invalid_2fa_method": "Kehtetu 2FA meetod (kontrolli telefoni teel).", + "invalid_2fa_method": "Kehtetu kaheastmelise tuvastuse meetod (kontrolli telefonistl).", "invalid_login": "Vale kasutajanimi, palun proovi uuesti." }, "step": { "2fa": { "data": { - "2fa": "2FA PIN" + "2fa": "Kaheastmelise tuvastuse PIN" }, "description": "", "title": "Kaheastmeline autentimine" diff --git a/homeassistant/components/hangouts/translations/fr.json b/homeassistant/components/hangouts/translations/fr.json index 2e8bec54c34..68e652db309 100644 --- a/homeassistant/components/hangouts/translations/fr.json +++ b/homeassistant/components/hangouts/translations/fr.json @@ -12,7 +12,7 @@ "step": { "2fa": { "data": { - "2fa": "Code PIN d'authentification \u00e0 2 facteurs" + "2fa": "Code NIP d'authentification \u00e0 2 facteurs" }, "description": "Vide", "title": "Authentification \u00e0 2 facteurs" diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index 9a9f5b41598..b81e3fcf0dd 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "A Google Hangouts m\u00e1r konfigur\u00e1lva van", - "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt." + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "invalid_2fa": "\u00c9rv\u00e9nytelen K\u00e9tfaktoros hiteles\u00edt\u00e9s, pr\u00f3b\u00e1ld \u00fajra.", @@ -14,7 +14,6 @@ "data": { "2fa": "2FA Pin" }, - "description": "\u00dcres", "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" }, "user": { @@ -22,7 +21,6 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "description": "\u00dcres", "title": "Google Hangouts Bejelentkez\u00e9s" } } diff --git a/homeassistant/components/hangouts/translations/id.json b/homeassistant/components/hangouts/translations/id.json index 1bcfeaeba50..39c68dda211 100644 --- a/homeassistant/components/hangouts/translations/id.json +++ b/homeassistant/components/hangouts/translations/id.json @@ -1,29 +1,30 @@ { "config": { "abort": { - "already_configured": "Google Hangouts sudah dikonfigurasikan", - "unknown": "Kesalahan tidak dikenal terjadi." + "already_configured": "Layanan sudah dikonfigurasi", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { - "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, silakan coba lagi.", - "invalid_2fa_method": "Metode 2FA Tidak Sah (Verifikasi di Ponsel).", - "invalid_login": "Login tidak valid, silahkan coba lagi." + "invalid_2fa": "Autentikasi 2 Faktor Tidak Valid, coba lagi.", + "invalid_2fa_method": "Metode 2FA Tidak Valid (Verifikasikan di Ponsel).", + "invalid_login": "Info Masuk Tidak Valid, coba lagi." }, "step": { "2fa": { "data": { - "2fa": "Pin 2FA" + "2fa": "PIN 2FA" }, "description": "Kosong", - "title": "2-Faktor-Otentikasi" + "title": "Autentikasi Dua Faktor" }, "user": { "data": { - "email": "Alamat email", - "password": "Kata sandi" + "authorization_code": "Kode Otorisasi (diperlukan untuk autentikasi manual)", + "email": "Email", + "password": "Kata Sandi" }, "description": "Kosong", - "title": "Google Hangouts Login" + "title": "Info Masuk Google Hangouts" } } } diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json index 4831d51ef12..3e89327ca30 100644 --- a/homeassistant/components/hangouts/translations/it.json +++ b/homeassistant/components/hangouts/translations/it.json @@ -12,7 +12,7 @@ "step": { "2fa": { "data": { - "2fa": "2FA Pin" + "2fa": "2FA PIN" }, "description": "Vuoto", "title": "Autenticazione a due fattori" diff --git a/homeassistant/components/hangouts/translations/ko.json b/homeassistant/components/hangouts/translations/ko.json index 3c23effaf4f..56c3c577a89 100644 --- a/homeassistant/components/hangouts/translations/ko.json +++ b/homeassistant/components/hangouts/translations/ko.json @@ -7,7 +7,7 @@ "error": { "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "invalid_2fa_method": "2\ub2e8\uacc4 \uc778\uc99d \ubc29\ubc95\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. (\uc804\ud654\uae30\uc5d0\uc11c \ud655\uc778)", - "invalid_login": "\uc798\ubabb\ub41c \ub85c\uadf8\uc778\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "invalid_login": "\ub85c\uadf8\uc778\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "step": { "2fa": { diff --git a/homeassistant/components/hangouts/translations/nl.json b/homeassistant/components/hangouts/translations/nl.json index fac77660251..456d2193922 100644 --- a/homeassistant/components/hangouts/translations/nl.json +++ b/homeassistant/components/hangouts/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Google Hangouts is al geconfigureerd", - "unknown": "Onbekende fout opgetreden." + "already_configured": "Service is al geconfigureerd", + "unknown": "Onverwachte fout" }, "error": { "invalid_2fa": "Ongeldige twee-factor-authenticatie, probeer het opnieuw.", @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Autorisatiecode (vereist voor handmatige authenticatie)", - "email": "E-mailadres", + "email": "E-mail", "password": "Wachtwoord" }, "description": "Leeg", diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json index fa341509634..d4fe4dbb5a6 100644 --- a/homeassistant/components/hangouts/translations/no.json +++ b/homeassistant/components/hangouts/translations/no.json @@ -6,13 +6,13 @@ }, "error": { "invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.", - "invalid_2fa_method": "Ugyldig totrinnsbekreftelse-metode (Bekreft p\u00e5 telefon)", + "invalid_2fa_method": "Ugyldig 2FA-metode (bekreft p\u00e5 telefon).", "invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen." }, "step": { "2fa": { "data": { - "2fa": "Totrinnsbekreftelse Pin" + "2fa": "2FA PIN" }, "description": "", "title": "Totrinnsbekreftelse" diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json index ff60deeece2..8fb7e9e64d9 100644 --- a/homeassistant/components/hangouts/translations/pl.json +++ b/homeassistant/components/hangouts/translations/pl.json @@ -12,7 +12,7 @@ "step": { "2fa": { "data": { - "2fa": "PIN" + "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" }, "description": "Pusty", "title": "Uwierzytelnianie dwusk\u0142adnikowe" diff --git a/homeassistant/components/hangouts/translations/zh-Hant.json b/homeassistant/components/hangouts/translations/zh-Hant.json index 62a220eaa94..678aacc5b62 100644 --- a/homeassistant/components/hangouts/translations/zh-Hant.json +++ b/homeassistant/components/hangouts/translations/zh-Hant.json @@ -5,17 +5,17 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "invalid_2fa": "\u96d9\u91cd\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", - "invalid_2fa_method": "\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002", + "invalid_2fa": "\u96d9\u91cd\u8a8d\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_2fa_method": "\u5169\u968e\u6bb5\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002", "invalid_login": "\u767b\u5165\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, "step": { "2fa": { "data": { - "2fa": "\u8a8d\u8b49\u78bc" + "2fa": "\u5169\u968e\u6bb5\u8a8d\u8b49\u78bc" }, "description": "\u7a7a\u767d", - "title": "\u96d9\u91cd\u9a57\u8b49" + "title": "\u96d9\u91cd\u8a8d\u8b49" }, "user": { "data": { diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 8445c7be937..c4056044ca0 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -48,9 +48,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.add_update_listener(_update_listener) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -108,8 +108,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 899edeb8a91..a91c1f3b5ca 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -119,6 +119,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.harmony_config, {} ) + self._set_confirm_only() return self.async_show_form( step_id="link", errors=errors, diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index ee4a454847e..d7b4d8248ed 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -6,9 +6,7 @@ PLATFORMS = ["remote", "switch"] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" -ATTR_ACTIVITY_LIST = "activity_list" ATTR_DEVICES_LIST = "devices_list" ATTR_LAST_ACTIVITY = "last_activity" -ATTR_CURRENT_ACTIVITY = "current_activity" ATTR_ACTIVITY_STARTING = "activity_starting" PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 9b3d53c21fa..a09f32ee95e 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -12,6 +12,7 @@ from homeassistant.components.remote import ( ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, + SUPPORT_ACTIVITY, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID @@ -24,9 +25,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .connection_state import ConnectionStateMixin from .const import ( ACTIVITY_POWER_OFF, - ATTR_ACTIVITY_LIST, ATTR_ACTIVITY_STARTING, - ATTR_CURRENT_ACTIVITY, ATTR_DEVICES_LIST, ATTR_LAST_ACTIVITY, DOMAIN, @@ -100,6 +99,11 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): self._last_activity = None self._config_path = out_path + @property + def supported_features(self): + """Supported features for the remote.""" + return SUPPORT_ACTIVITY + async def _async_update_options(self, data): """Change options when the options flow does.""" if ATTR_DELAY_SECS in data: @@ -179,12 +183,20 @@ class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): return False @property - def device_state_attributes(self): + def current_activity(self): + """Return the current activity.""" + return self._current_activity + + @property + def activity_list(self): + """Return the available activities.""" + return self._data.activity_names + + @property + def extra_state_attributes(self): """Add platform specific attributes.""" return { ATTR_ACTIVITY_STARTING: self._activity_starting, - ATTR_CURRENT_ACTIVITY: self._current_activity, - ATTR_ACTIVITY_LIST: self._data.activity_names, ATTR_DEVICES_LIST: self._data.device_names, ATTR_LAST_ACTIVITY: self._last_activity, } diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json index cbf055e2fba..a9cb6ccecee 100644 --- a/homeassistant/components/harmony/translations/hu.json +++ b/homeassistant/components/harmony/translations/hu.json @@ -1,5 +1,12 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/harmony/translations/id.json b/homeassistant/components/harmony/translations/id.json new file mode 100644 index 00000000000..0d2991b1feb --- /dev/null +++ b/homeassistant/components/harmony/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Ingin menyiapkan {name} ({host})?", + "title": "Siapkan Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Host", + "name": "Nama Hub" + }, + "title": "Siapkan Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Aktivitas default yang akan dijalankan jika tidak ada yang ditentukan.", + "delay_secs": "Penundaan antara mengirim perintah." + }, + "description": "Sesuaikan Opsi Hub Harmony" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json index 63d8026d9c2..33cbeca8893 100644 --- a/homeassistant/components/harmony/translations/nl.json +++ b/homeassistant/components/harmony/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "flow_title": "Logitech Harmony Hub {name}", @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Hostnaam of IP-adres", + "host": "Host", "name": "Naam van hub" }, "title": "Logitech Harmony Hub instellen" diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 5b40d7142f1..a5a2a1886d7 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,24 +1,31 @@ """Support for Hass.io.""" +from __future__ import annotations + +import asyncio from datetime import timedelta import logging import os -from typing import Optional +from typing import Any import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG import homeassistant.config as conf_util +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, + ATTR_SERVICE, EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, ) -from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.core import DOMAIN as HASS_DOMAIN, Config, HomeAssistant, callback 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 @@ -32,7 +39,11 @@ from .const import ( ATTR_HOMEASSISTANT, ATTR_INPUT, ATTR_PASSWORD, + ATTR_REPOSITORY, + ATTR_SLUG, ATTR_SNAPSHOT, + ATTR_URL, + ATTR_VERSION, DOMAIN, ) from .discovery import async_setup_discovery_view @@ -46,6 +57,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 +PLATFORMS = ["binary_sensor", "sensor"] CONF_FRONTEND_REPO = "development_repo" @@ -62,9 +74,12 @@ DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) +ADDONS_COORDINATOR = "hassio_addons_coordinator" + SERVICE_ADDON_START = "addon_start" SERVICE_ADDON_STOP = "addon_stop" SERVICE_ADDON_RESTART = "addon_restart" +SERVICE_ADDON_UPDATE = "addon_update" SERVICE_ADDON_STDIN = "addon_stdin" SERVICE_HOST_SHUTDOWN = "host_shutdown" SERVICE_HOST_REBOOT = "host_reboot" @@ -110,6 +125,7 @@ MAP_SERVICE_API = { SERVICE_ADDON_START: ("/addons/{addon}/start", SCHEMA_ADDON, 60, False), SERVICE_ADDON_STOP: ("/addons/{addon}/stop", SCHEMA_ADDON, 60, False), SERVICE_ADDON_RESTART: ("/addons/{addon}/restart", SCHEMA_ADDON, 60, False), + SERVICE_ADDON_UPDATE: ("/addons/{addon}/update", SCHEMA_ADDON, 60, False), SERVICE_ADDON_STDIN: ("/addons/{addon}/stdin", SCHEMA_ADDON_STDIN, 60, False), SERVICE_HOST_SHUTDOWN: ("/host/shutdown", SCHEMA_NO_DATA, 60, False), SERVICE_HOST_REBOOT: ("/host/reboot", SCHEMA_NO_DATA, 60, False), @@ -145,6 +161,16 @@ async def async_get_addon_info(hass: HomeAssistantType, slug: str) -> dict: return await hassio.get_addon_info(slug) +@bind_hass +async def async_update_diagnostics(hass: HomeAssistantType, diagnostics: bool) -> dict: + """Update Supervisor diagnostics toggle. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + return await hassio.update_diagnostics(diagnostics) + + @bind_hass @api_data async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: @@ -222,7 +248,7 @@ async def async_set_addon_options( @bind_hass async def async_get_addon_discovery_info( hass: HomeAssistantType, slug: str -) -> Optional[dict]: +) -> dict | None: """Return discovery data for an add-on.""" hassio = hass.data[DOMAIN] data = await hassio.retrieve_discovery_messages() @@ -313,13 +339,17 @@ def get_supervisor_ip(): return os.environ["SUPERVISOR"].partition(":")[0] -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up the Hass.io component.""" # Check local setup for env in ("HASSIO", "HASSIO_TOKEN"): if os.environ.get(env): continue _LOGGER.error("Missing %s environment variable", env) + if config_entries := hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.async_remove(config_entries[0].entry_id) + ) return False async_load_websocket_api(hass) @@ -429,6 +459,8 @@ async def async_setup(hass, config): hass.data[DATA_CORE_INFO] = await hassio.get_core_info() hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() hass.data[DATA_OS_INFO] = await hassio.get_os_info() + if ADDONS_COORDINATOR in hass.data: + await hass.data[ADDONS_COORDINATOR].async_refresh() except HassioAPIError as err: _LOGGER.warning("Can't read last version: %s", err) @@ -482,4 +514,144 @@ async def async_setup(hass, config): # Init add-on ingress panels await async_setup_addon_panel(hass, hassio) + hass.async_create_task( + hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up a config entry.""" + dev_reg = await async_get_registry(hass) + coordinator = HassioDataUpdateCoordinator(hass, config_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) + ) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, 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 + ] + ) + ) + + # Pop add-on data + hass.data.pop(ADDONS_COORDINATOR, None) + + return unload_ok + + +@callback +def async_register_addons_in_dev_reg( + entry_id: str, dev_reg: DeviceRegistry, addons: list[dict[str, Any]] +) -> None: + """Register addons in the device registry.""" + for addon in addons: + params = { + "config_entry_id": entry_id, + "identifiers": {(DOMAIN, addon[ATTR_SLUG])}, + "model": "Home Assistant Add-on", + "sw_version": addon[ATTR_VERSION], + "name": addon[ATTR_NAME], + "entry_type": ATTR_SERVICE, + } + if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): + params["manufacturer"] = manufacturer + dev_reg.async_get_or_create(**params) + + +@callback +def async_register_os_in_dev_reg( + entry_id: str, dev_reg: DeviceRegistry, os_dict: dict[str, Any] +) -> None: + """Register OS in the device registry.""" + params = { + "config_entry_id": entry_id, + "identifiers": {(DOMAIN, "OS")}, + "manufacturer": "Home Assistant", + "model": "Home Assistant Operating System", + "sw_version": os_dict[ATTR_VERSION], + "name": "Home Assistant Operating System", + "entry_type": ATTR_SERVICE, + } + dev_reg.async_get_or_create(**params) + + +@callback +def async_remove_addons_from_dev_reg( + dev_reg: DeviceRegistry, addons: list[dict[str, Any]] +) -> None: + """Remove addons from the device registry.""" + for addon_slug in addons: + if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}): + dev_reg.async_remove_device(dev.id) + + +class HassioDataUpdateCoordinator(DataUpdateCoordinator): + """Class to retrieve Hass.io status.""" + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: DeviceRegistry + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_method=self._async_update_data, + ) + self.data = {} + self.entry_id = config_entry.entry_id + self.dev_reg = dev_reg + self.is_hass_os = "hassos" in get_info(self.hass) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + new_data = {} + addon_data = get_supervisor_info(self.hass) + + new_data["addons"] = { + addon[ATTR_SLUG]: addon for addon in addon_data.get("addons", []) + } + if self.is_hass_os: + new_data["os"] = get_os_info(self.hass) + + # If this is the initial refresh, register all addons and return the dict + if not self.data: + async_register_addons_in_dev_reg( + self.entry_id, self.dev_reg, new_data["addons"].values() + ) + if self.is_hass_os: + async_register_os_in_dev_reg( + self.entry_id, self.dev_reg, new_data["os"] + ) + return new_data + + # Remove add-ons that are no longer installed from device registry + if removed_addons := list(set(self.data["addons"]) - set(new_data["addons"])): + async_remove_addons_from_dev_reg(self.dev_reg, removed_addons) + + # If there are new add-ons, we should reload the config entry so we can + # create new devices and entities. We can return an empty dict because + # coordinator will be recreated. + if list(set(new_data["addons"]) - set(self.data["addons"])): + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry_id) + ) + return {} + + return new_data diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py new file mode 100644 index 00000000000..b6faf566807 --- /dev/null +++ b/homeassistant/components/hassio/binary_sensor.py @@ -0,0 +1,52 @@ +"""Binary sensor platform for Hass.io addons.""" +from __future__ import annotations + +from typing import Callable + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from . import ADDONS_COORDINATOR +from .const import ATTR_UPDATE_AVAILABLE +from .entity import HassioAddonEntity, HassioOSEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: + """Binary sensor set up for Hass.io config entry.""" + coordinator = hass.data[ADDONS_COORDINATOR] + + entities = [ + HassioAddonBinarySensor( + coordinator, addon, ATTR_UPDATE_AVAILABLE, "Update Available" + ) + for addon in coordinator.data["addons"].values() + ] + if coordinator.is_hass_os: + entities.append( + HassioOSBinarySensor(coordinator, ATTR_UPDATE_AVAILABLE, "Update Available") + ) + async_add_entities(entities) + + +class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): + """Binary sensor to track whether an update is available for a Hass.io add-on.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.addon_info[self.attribute_name] + + +class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): + """Binary sensor to track whether an update is available for Hass.io OS.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.os_info[self.attribute_name] diff --git a/homeassistant/components/hassio/config_flow.py b/homeassistant/components/hassio/config_flow.py new file mode 100644 index 00000000000..acc39f4cf91 --- /dev/null +++ b/homeassistant/components/hassio/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for Home Assistant Supervisor integration.""" +import logging + +from homeassistant import config_entries + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Supervisor.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_system(self, user_input=None): + """Handle the initial step.""" + # We only need one Hass.io config entry + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Supervisor", data={}) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index b2878c8143f..417a62a1a8c 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -29,7 +29,6 @@ X_INGRESS_PATH = "X-Ingress-Path" X_HASS_USER_ID = "X-Hass-User-ID" X_HASS_IS_ADMIN = "X-Hass-Is-Admin" - WS_TYPE = "type" WS_ID = "id" @@ -38,3 +37,11 @@ WS_TYPE_EVENT = "supervisor/event" WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" + +# Add-on keys +ATTR_VERSION = "version" +ATTR_VERSION_LATEST = "version_latest" +ATTR_UPDATE_AVAILABLE = "update_available" +ATTR_SLUG = "slug" +ATTR_URL = "url" +ATTR_REPOSITORY = "repository" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py new file mode 100644 index 00000000000..5f35235bb5d --- /dev/null +++ b/homeassistant/components/hassio/entity.py @@ -0,0 +1,95 @@ +"""Base for Hass.io entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import DOMAIN, HassioDataUpdateCoordinator +from .const import ATTR_SLUG + + +class HassioAddonEntity(CoordinatorEntity): + """Base entity for a Hass.io add-on.""" + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + addon: dict[str, Any], + attribute_name: str, + sensor_name: str, + ) -> None: + """Initialize base entity.""" + self.addon_slug = addon[ATTR_SLUG] + self.addon_name = addon[ATTR_NAME] + self._data_key = "addons" + self.attribute_name = attribute_name + self.sensor_name = sensor_name + super().__init__(coordinator) + + @property + def addon_info(self) -> dict[str, Any]: + """Return add-on info.""" + return self.coordinator.data[self._data_key][self.addon_slug] + + @property + def name(self) -> str: + """Return entity name.""" + return f"{self.addon_name}: {self.sensor_name}" + + @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 unique_id(self) -> str: + """Return unique ID for entity.""" + return f"{self.addon_slug}_{self.attribute_name}" + + @property + def device_info(self) -> dict[str, Any]: + """Return device specific attributes.""" + return {"identifiers": {(DOMAIN, self.addon_slug)}} + + +class HassioOSEntity(CoordinatorEntity): + """Base Entity for Hass.io OS.""" + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + attribute_name: str, + sensor_name: str, + ) -> None: + """Initialize base entity.""" + self._data_key = "os" + self.attribute_name = attribute_name + self.sensor_name = sensor_name + super().__init__(coordinator) + + @property + def os_info(self) -> dict[str, Any]: + """Return OS info.""" + return self.coordinator.data[self._data_key] + + @property + def name(self) -> str: + """Return entity name.""" + return f"Home Assistant Operating System: {self.sensor_name}" + + @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 unique_id(self) -> str: + """Return unique ID for entity.""" + return f"home_assistant_os_{self.attribute_name}" + + @property + def device_info(self) -> dict[str, Any]: + """Return device specific attributes.""" + return {"identifiers": {(DOMAIN, "OS")}} diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 6bc3cb345a5..90077261185 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -4,7 +4,6 @@ import logging import os import aiohttp -import async_timeout from homeassistant.components.http import ( CONF_SERVER_HOST, @@ -52,7 +51,12 @@ def api_data(funct): class HassIO: """Small API wrapper for Hass.io.""" - def __init__(self, loop, websession, ip): + def __init__( + self, + loop: asyncio.AbstractEventLoop, + websession: aiohttp.ClientSession, + ip: str, + ) -> None: """Initialize Hass.io API.""" self.loop = loop self.websession = websession @@ -181,26 +185,36 @@ class HassIO: """ return self.send_command("/supervisor/options", payload={"timezone": timezone}) + @_api_bool + def update_diagnostics(self, diagnostics: bool): + """Update Supervisor diagnostics setting. + + This method return a coroutine. + """ + return self.send_command( + "/supervisor/options", payload={"diagnostics": diagnostics} + ) + async def send_command(self, command, method="post", payload=None, timeout=10): """Send API command to Hass.io. This method is a coroutine. """ try: - with async_timeout.timeout(timeout): - request = await self.websession.request( - method, - f"http://{self._ip}{command}", - json=payload, - headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")}, - ) + request = await self.websession.request( + method, + f"http://{self._ip}{command}", + json=payload, + headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")}, + timeout=aiohttp.ClientTimeout(total=timeout), + ) - if request.status not in (HTTP_OK, HTTP_BAD_REQUEST): - _LOGGER.error("%s return code %d", command, request.status) - raise HassioAPIError() + if request.status not in (HTTP_OK, HTTP_BAD_REQUEST): + _LOGGER.error("%s return code %d", command, request.status) + raise HassioAPIError() - answer = await request.json() - return answer + answer = await request.json() + return answer except asyncio.TimeoutError: _LOGGER.error("Timeout on %s request", command) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 2aa05ae6ab4..e1bd1cb095c 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,9 +1,10 @@ """HTTP Support for Hass.io.""" +from __future__ import annotations + import asyncio import logging import os import re -from typing import Dict, Union import aiohttp from aiohttp import web @@ -57,7 +58,7 @@ class HassIOView(HomeAssistantView): async def _handle( self, request: web.Request, path: str - ) -> Union[web.Response, web.StreamResponse]: + ) -> web.Response | web.StreamResponse: """Route data to Hass.io.""" hass = request.app["hass"] if _need_auth(hass, path) and not request[KEY_AUTHENTICATED]: @@ -71,7 +72,7 @@ class HassIOView(HomeAssistantView): async def _command_proxy( self, path: str, request: web.Request - ) -> Union[web.Response, web.StreamResponse]: + ) -> web.Response | web.StreamResponse: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. @@ -131,7 +132,7 @@ class HassIOView(HomeAssistantView): raise HTTPBadGateway() -def _init_header(request: web.Request) -> Dict[str, str]: +def _init_header(request: web.Request) -> dict[str, str]: """Create initial header.""" headers = { X_HASSIO: os.environ.get("HASSIO_TOKEN", ""), diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index c69d2078468..1f0a49ae497 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -1,9 +1,10 @@ """Hass.io Add-on ingress service.""" +from __future__ import annotations + import asyncio from ipaddress import ip_address import logging import os -from typing import Dict, Union import aiohttp from aiohttp import hdrs, web @@ -46,7 +47,7 @@ class HassIOIngress(HomeAssistantView): async def _handle( self, request: web.Request, token: str, path: str - ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: + ) -> web.Response | web.StreamResponse | web.WebSocketResponse: """Route data to Hass.io ingress service.""" try: # Websocket @@ -114,7 +115,7 @@ class HassIOIngress(HomeAssistantView): async def _handle_request( self, request: web.Request, token: str, path: str - ) -> Union[web.Response, web.StreamResponse]: + ) -> web.Response | web.StreamResponse: """Ingress route for request.""" url = self._create_url(token, path) data = await request.read() @@ -159,9 +160,7 @@ class HassIOIngress(HomeAssistantView): return response -def _init_header( - request: web.Request, token: str -) -> Union[CIMultiDict, Dict[str, str]]: +def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, str]: """Create initial header.""" headers = {} @@ -208,7 +207,7 @@ def _init_header( return headers -def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: +def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]: """Create response header.""" headers = {} diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py new file mode 100644 index 00000000000..c41c0dc5090 --- /dev/null +++ b/homeassistant/components/hassio/sensor.py @@ -0,0 +1,55 @@ +"""Sensor platform for Hass.io addons.""" +from __future__ import annotations + +from typing import Callable + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from . import ADDONS_COORDINATOR +from .const import ATTR_VERSION, ATTR_VERSION_LATEST +from .entity import HassioAddonEntity, HassioOSEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], +) -> None: + """Sensor set up for Hass.io config entry.""" + coordinator = hass.data[ADDONS_COORDINATOR] + + entities = [] + + for attribute_name, sensor_name in ( + (ATTR_VERSION, "Version"), + (ATTR_VERSION_LATEST, "Newest Version"), + ): + for addon in coordinator.data["addons"].values(): + entities.append( + HassioAddonSensor(coordinator, addon, attribute_name, sensor_name) + ) + if coordinator.is_hass_os: + entities.append(HassioOSSensor(coordinator, attribute_name, sensor_name)) + + async_add_entities(entities) + + +class HassioAddonSensor(HassioAddonEntity, SensorEntity): + """Sensor to track a Hass.io add-on attribute.""" + + @property + def state(self) -> str: + """Return state of entity.""" + return self.addon_info[self.attribute_name] + + +class HassioOSSensor(HassioOSEntity, SensorEntity): + """Sensor to track a Hass.io add-on attribute.""" + + @property + def state(self) -> str: + """Return state of entity.""" + return self.os_info[self.attribute_name] diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 3570a857c55..0652b65d6e2 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -46,6 +46,18 @@ addon_stop: selector: addon: +addon_update: + name: Update add-on. + description: Update add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on. + fields: + addon: + name: Add-on + required: true + description: The add-on slug. + example: core_ssh + selector: + addon: + host_reboot: name: Reboot the host system. description: Reboot the host system. diff --git a/homeassistant/components/hassio/translations/af.json b/homeassistant/components/hassio/translations/af.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/af.json +++ b/homeassistant/components/hassio/translations/af.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 19b4316c9ce..d2e712c230d 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -15,5 +15,5 @@ "version_api": "Versi\u00f3 d'APIs" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json index cf1a28c3cc2..eb64ed58baa 100644 --- a/homeassistant/components/hassio/translations/cs.json +++ b/homeassistant/components/hassio/translations/cs.json @@ -15,5 +15,5 @@ "version_api": "Verze API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/cy.json b/homeassistant/components/hassio/translations/cy.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/cy.json +++ b/homeassistant/components/hassio/translations/cy.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/da.json b/homeassistant/components/hassio/translations/da.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/da.json +++ b/homeassistant/components/hassio/translations/da.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index 939821edb54..a0d02f4a7b9 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -14,5 +14,5 @@ "version_api": "Versions-API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/el.json b/homeassistant/components/hassio/translations/el.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/el.json +++ b/homeassistant/components/hassio/translations/el.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 230e0c11fea..16911be4110 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -15,5 +15,5 @@ "version_api": "Version API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/es-419.json b/homeassistant/components/hassio/translations/es-419.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/es-419.json +++ b/homeassistant/components/hassio/translations/es-419.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 5faf32e515f..f3bdf14c446 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -15,5 +15,5 @@ "version_api": "Versi\u00f3n del API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index 9e5e776013f..9d3ef08afbe 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -15,5 +15,5 @@ "version_api": "API versioon" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/eu.json b/homeassistant/components/hassio/translations/eu.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/eu.json +++ b/homeassistant/components/hassio/translations/eu.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fa.json b/homeassistant/components/hassio/translations/fa.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/fa.json +++ b/homeassistant/components/hassio/translations/fa.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fi.json b/homeassistant/components/hassio/translations/fi.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/fi.json +++ b/homeassistant/components/hassio/translations/fi.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index cef14b258c4..e4fe8a63bba 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -7,13 +7,13 @@ "docker_version": "Version de Docker", "healthy": "Sain", "host_os": "Syst\u00e8me d'exploitation h\u00f4te", - "installed_addons": "Add-ons install\u00e9s", - "supervisor_api": "API du superviseur", - "supervisor_version": "Version du supervisor", + "installed_addons": "Modules compl\u00e9mentaires install\u00e9s", + "supervisor_api": "API du Supervisor", + "supervisor_version": "Version du Supervisor", "supported": "Prise en charge", "update_channel": "Mise \u00e0 jour", "version_api": "Version API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json index 981cb51c83a..80c1a0c48ee 100644 --- a/homeassistant/components/hassio/translations/he.json +++ b/homeassistant/components/hassio/translations/he.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hr.json b/homeassistant/components/hassio/translations/hr.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/hr.json +++ b/homeassistant/components/hassio/translations/hr.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 216e8d391b6..e0fc98408d4 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -7,12 +7,12 @@ "healthy": "Eg\u00e9szs\u00e9ges", "host_os": "Gazdag\u00e9p oper\u00e1ci\u00f3s rendszer", "installed_addons": "Telep\u00edtett kieg\u00e9sz\u00edt\u0151k", - "supervisor_api": "Adminisztr\u00e1tor API", - "supervisor_version": "Adminisztr\u00e1tor verzi\u00f3", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor verzi\u00f3", "supported": "T\u00e1mogatott", "update_channel": "Friss\u00edt\u00e9si csatorna", "version_api": "API verzi\u00f3" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/hy.json b/homeassistant/components/hassio/translations/hy.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/hy.json +++ b/homeassistant/components/hassio/translations/hy.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/id.json b/homeassistant/components/hassio/translations/id.json new file mode 100644 index 00000000000..b95ffb35d81 --- /dev/null +++ b/homeassistant/components/hassio/translations/id.json @@ -0,0 +1,19 @@ +{ + "system_health": { + "info": { + "board": "Board", + "disk_total": "Total Disk", + "disk_used": "Disk Digunakan", + "docker_version": "Versi Docker", + "healthy": "Kesehatan", + "host_os": "Sistem Operasi Host", + "installed_addons": "Add-on yang Diinstal", + "supervisor_api": "API Supervisor", + "supervisor_version": "Versi Supervisor", + "supported": "Didukung", + "update_channel": "Kanal Pembaruan", + "version_api": "API Versi" + } + }, + "title": "Home Assistant Supervisor" +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/is.json b/homeassistant/components/hassio/translations/is.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/is.json +++ b/homeassistant/components/hassio/translations/is.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 385a0eedff2..86d573cba40 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -8,12 +8,12 @@ "healthy": "Integrit\u00e0", "host_os": "Sistema Operativo Host", "installed_addons": "Componenti aggiuntivi installati", - "supervisor_api": "API Supervisore", - "supervisor_version": "Versione Supervisore", + "supervisor_api": "API Supervisor", + "supervisor_version": "Versione Supervisor", "supported": "Supportato", "update_channel": "Canale di aggiornamento", "version_api": "Versione API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ko.json b/homeassistant/components/hassio/translations/ko.json index 981cb51c83a..aba9a665f70 100644 --- a/homeassistant/components/hassio/translations/ko.json +++ b/homeassistant/components/hassio/translations/ko.json @@ -1,3 +1,19 @@ { - "title": "Hass.io" + "system_health": { + "info": { + "board": "\ubcf4\ub4dc \uc720\ud615", + "disk_total": "\ub514\uc2a4\ud06c \ucd1d \uc6a9\ub7c9", + "disk_used": "\ub514\uc2a4\ud06c \uc0ac\uc6a9\ub7c9", + "docker_version": "Docker \ubc84\uc804", + "healthy": "\uc2dc\uc2a4\ud15c \uc0c1\ud0dc", + "host_os": "\ud638\uc2a4\ud2b8 \uc6b4\uc601 \uccb4\uc81c", + "installed_addons": "\uc124\uce58\ub41c \uc560\ub4dc\uc628", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor \ubc84\uc804", + "supported": "\uc9c0\uc6d0 \uc5ec\ubd80", + "update_channel": "\uc5c5\ub370\uc774\ud2b8 \ucc44\ub110", + "version_api": "\ubc84\uc804 API" + } + }, + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lb.json b/homeassistant/components/hassio/translations/lb.json index 54aae5a2c59..c0d0f42ed94 100644 --- a/homeassistant/components/hassio/translations/lb.json +++ b/homeassistant/components/hassio/translations/lb.json @@ -15,5 +15,5 @@ "version_api": "API Versioun" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lt.json b/homeassistant/components/hassio/translations/lt.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/lt.json +++ b/homeassistant/components/hassio/translations/lt.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/lv.json b/homeassistant/components/hassio/translations/lv.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/lv.json +++ b/homeassistant/components/hassio/translations/lv.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/nb.json b/homeassistant/components/hassio/translations/nb.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/hassio/translations/nb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index fca08d49d7c..7224857a10c 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "board": "Bord", "disk_total": "Totale schijfruimte", "disk_used": "Gebruikte schijfruimte", "docker_version": "Docker versie", @@ -14,5 +15,5 @@ "version_api": "API Versie" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/nn.json b/homeassistant/components/hassio/translations/nn.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/nn.json +++ b/homeassistant/components/hassio/translations/nn.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index e652f76a12c..9f0c5ba89b2 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -15,5 +15,5 @@ "version_api": "Versjon API" } }, - "title": "" + "title": "Home Assistant veileder" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 10ee7c9d16c..5266d640d7c 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -15,5 +15,5 @@ "version_api": "Wersja API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json index 06083bae759..326560409e4 100644 --- a/homeassistant/components/hassio/translations/pt.json +++ b/homeassistant/components/hassio/translations/pt.json @@ -13,5 +13,5 @@ "supported": "Suportado" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ro.json b/homeassistant/components/hassio/translations/ro.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/ro.json +++ b/homeassistant/components/hassio/translations/ro.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 4f9b16621fc..56c3522ba3c 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -15,5 +15,5 @@ "version_api": "\u0412\u0435\u0440\u0441\u0438\u044f API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sk.json b/homeassistant/components/hassio/translations/sk.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/sk.json +++ b/homeassistant/components/hassio/translations/sk.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sl.json b/homeassistant/components/hassio/translations/sl.json index eb2f5f7ca8b..cfc71ce0832 100644 --- a/homeassistant/components/hassio/translations/sl.json +++ b/homeassistant/components/hassio/translations/sl.json @@ -14,5 +14,5 @@ "version_api": "API razli\u010dica" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/sv.json +++ b/homeassistant/components/hassio/translations/sv.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/th.json b/homeassistant/components/hassio/translations/th.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/th.json +++ b/homeassistant/components/hassio/translations/th.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index f2c2d52f60d..06a8d3fd661 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -15,5 +15,5 @@ "version_api": "S\u00fcr\u00fcm API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/uk.json b/homeassistant/components/hassio/translations/uk.json index 19a40730897..d25ad6e7979 100644 --- a/homeassistant/components/hassio/translations/uk.json +++ b/homeassistant/components/hassio/translations/uk.json @@ -15,5 +15,5 @@ "version_api": "\u0412\u0435\u0440\u0441\u0456\u044f API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/vi.json b/homeassistant/components/hassio/translations/vi.json index 981cb51c83a..91588c5529a 100644 --- a/homeassistant/components/hassio/translations/vi.json +++ b/homeassistant/components/hassio/translations/vi.json @@ -1,3 +1,3 @@ { - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json index 0d74360b8f3..a48cbeb95a8 100644 --- a/homeassistant/components/hassio/translations/zh-Hans.json +++ b/homeassistant/components/hassio/translations/zh-Hans.json @@ -15,5 +15,5 @@ "version_api": "API \u7248\u672c" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index 574b82358d6..b8b3a1e7b93 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -7,7 +7,7 @@ "docker_version": "Docker \u7248\u672c", "healthy": "\u5065\u5eb7\u5ea6", "host_os": "\u4e3b\u6a5f\u4f5c\u696d\u7cfb\u7d71", - "installed_addons": "\u5df2\u5b89\u88dd Add-on", + "installed_addons": "\u5df2\u5b89\u88dd\u9644\u52a0\u5143\u4ef6", "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor \u7248\u672c", "supported": "\u652f\u63f4", @@ -15,5 +15,5 @@ "version_api": "\u7248\u672c API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 0f5a9b5ebfd..55b369c2fde 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -6,7 +6,7 @@ from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -15,7 +15,6 @@ from homeassistant.const import ( HTTP_OK, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -54,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class HaveIBeenPwnedSensor(Entity): +class HaveIBeenPwnedSensor(SensorEntity): """Implementation of a HaveIBeenPwned sensor.""" def __init__(self, data, email): @@ -80,7 +79,7 @@ class HaveIBeenPwnedSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the sensor.""" val = {ATTR_ATTRIBUTION: ATTRIBUTION} if self._email not in self._data.data: diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index a1052b0440a..4376c7f1289 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -6,7 +6,7 @@ from telnetlib import Telnet import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -16,7 +16,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -60,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class HddTempSensor(Entity): +class HddTempSensor(SensorEntity): """Representation of a HDDTemp sensor.""" def __init__(self, name, disk, hddtemp): @@ -88,7 +87,7 @@ class HddTempSensor(Entity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._details is not None: return {ATTR_DEVICE: self._details[0], ATTR_MODEL: self._details[1]} diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index c272ad19c8d..c7dfd335c32 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,16 +1,12 @@ """Support for HDMI CEC.""" from collections import defaultdict -from functools import reduce +from functools import partial, reduce import logging import multiprocessing -from pycec.cec import CecAdapter # pylint: disable=import-error -from pycec.commands import ( # pylint: disable=import-error - CecCommand, - KeyPressCommand, - KeyReleaseCommand, -) -from pycec.const import ( # pylint: disable=import-error +from pycec.cec import CecAdapter +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( ADDR_AUDIOSYSTEM, ADDR_BROADCAST, ADDR_UNREGISTERED, @@ -25,8 +21,8 @@ from pycec.const import ( # pylint: disable=import-error STATUS_STILL, STATUS_STOP, ) -from pycec.network import HDMINetwork, PhysicalAddress # pylint: disable=import-error -from pycec.tcp import TcpAdapter # pylint: disable=import-error +from pycec.network import HDMINetwork, PhysicalAddress +from pycec.tcp import TcpAdapter import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER @@ -42,9 +38,10 @@ from homeassistant.const import ( STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, event import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -166,6 +163,9 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +WATCHDOG_INTERVAL = 120 +EVENT_HDMI_CEC_UNAVAILABLE = "hdmi_cec_unavailable" + def pad_physical_address(addr): """Right-pad a physical address.""" @@ -214,6 +214,18 @@ def setup(hass: HomeAssistant, base_config): adapter = CecAdapter(name=display_name[:12], activate_source=False) hdmi_network = HDMINetwork(adapter, loop=loop) + def _adapter_watchdog(now=None): + _LOGGER.debug("Reached _adapter_watchdog") + event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + if not adapter.initialized: + _LOGGER.info("Adapter not initialized; Trying to restart") + hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) + adapter.init() + + hdmi_network.set_initialized_callback( + partial(event.async_call_later, hass, WATCHDOG_INTERVAL, _adapter_watchdog) + ) + def _volume(call): """Increase/decrease volume and mute/unmute system.""" mute_key_mapping = { @@ -331,7 +343,7 @@ def setup(hass: HomeAssistant, base_config): def _shutdown(call): hdmi_network.stop() - def _start_cec(event): + def _start_cec(callback_event): """Register services and start HDMI network to watch for devices.""" hass.services.register( DOMAIN, SERVICE_SEND_COMMAND, _tx, SERVICE_SEND_COMMAND_SCHEMA @@ -368,6 +380,12 @@ class CecEntity(Entity): self._logical_address = logical self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) + def _hdmi_cec_unavailable(self, callback_event): + # Change state to unavailable. Without this, entity would remain in + # its last state, since the state changes are pushed. + self._state = STATE_UNAVAILABLE + self.schedule_update_ha_state(False) + def update(self): """Update device status.""" device = self._device @@ -387,6 +405,9 @@ class CecEntity(Entity): async def async_added_to_hass(self): """Register HDMI callbacks after initialization.""" self._device.set_update_callback(self._update) + self.hass.bus.async_listen( + EVENT_HDMI_CEC_UNAVAILABLE, self._hdmi_cec_unavailable + ) def _update(self, device=None): """Device status changed, schedule an update.""" @@ -454,7 +475,7 @@ class CecEntity(Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state_attr = {} if self.vendor_id is not None: diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 4c307582281..4f6975f52df 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -2,6 +2,6 @@ "domain": "hdmi_cec", "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", - "requirements": ["pyCEC==0.4.14"], - "codeowners": ["@newAM"] + "requirements": ["pyCEC==0.5.1"], + "codeowners": [] } diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index f81ee20afe3..c3cab6a8f98 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,12 +1,8 @@ """Support for HDMI CEC devices as media players.""" import logging -from pycec.commands import ( # pylint: disable=import-error - CecCommand, - KeyPressCommand, - KeyReleaseCommand, -) -from pycec.const import ( # pylint: disable=import-error +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( KEY_BACKWARD, KEY_FORWARD, KEY_MUTE_TOGGLE, diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index b3f3363818c..c8e1db0a10f 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -1,6 +1,7 @@ """Support for the PRT Heatmiser themostats using the V3 protocol.""" +from __future__ import annotations + import logging -from typing import List from heatmiserV3 import connection, heatmiser import voluptuous as vol @@ -103,7 +104,7 @@ class HeatmiserV3Thermostat(ClimateEntity): return self._hvac_mode @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 11020d1166e..a71d0d2de50 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -1,8 +1,9 @@ """Denon HEOS Media Player.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Dict from pyheos import Heos, HeosError, const as heos_const import voluptuous as vol @@ -191,7 +192,7 @@ class ControllerManager: # Update players self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) - def update_ids(self, mapped_ids: Dict[int, int]): + def update_ids(self, mapped_ids: dict[int, int]): """Update the IDs in the device and entity registry.""" # mapped_ids contains the mapped IDs (new:old) for new_id, old_id in mapped_ids.items(): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 15d3c4573db..6e271bf60cd 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -262,7 +262,7 @@ class HeosMediaPlayer(MediaPlayerEntity): } @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Get additional attribute about the state.""" return { "media_album_id": self._player.now_playing_media.album_id, diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index cf688d6fdeb..2fbce1993cd 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, diff --git a/homeassistant/components/heos/translations/id.json b/homeassistant/components/heos/translations/id.json new file mode 100644 index 00000000000..b7c57b46f66 --- /dev/null +++ b/homeassistant/components/heos/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Masukkan nama host atau alamat IP perangkat Heos (sebaiknya yang terhubung melalui kabel ke jaringan).", + "title": "Hubungkan ke Heos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/ko.json b/homeassistant/components/heos/translations/ko.json index d17cbd0e4b7..5e4057ae482 100644 --- a/homeassistant/components/heos/translations/ko.json +++ b/homeassistant/components/heos/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" @@ -12,7 +12,7 @@ "host": "\ud638\uc2a4\ud2b8" }, "description": "Heos \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c\ub85c \uc5f0\uacb0\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4)", - "title": "Heos \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "Heos\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index e51e7a067fc..4b8f765d08a 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,12 +1,14 @@ """Support for HERE travel time sensors.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging -from typing import Callable, Dict, Optional, Union +from typing import Callable import herepy import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -24,7 +26,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import DiscoveryInfoType import homeassistant.util.dt as dt @@ -143,9 +144,9 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform( hass: HomeAssistant, - config: Dict[str, Union[str, bool]], + config: dict[str, str | bool], async_add_entities: Callable, - discovery_info: Optional[DiscoveryInfoType] = None, + discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the HERE travel time platform.""" api_key = config[CONF_API_KEY] @@ -213,7 +214,7 @@ def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: return True -class HERETravelTimeSensor(Entity): +class HERETravelTimeSensor(SensorEntity): """Representation of a HERE travel time sensor.""" def __init__( @@ -223,7 +224,7 @@ class HERETravelTimeSensor(Entity): destination: str, origin_entity_id: str, destination_entity_id: str, - here_data: "HERETravelTimeData", + here_data: HERETravelTimeData, ) -> None: """Initialize the sensor.""" self._name = name @@ -255,11 +256,10 @@ class HERETravelTimeSensor(Entity): ) @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the sensor.""" - if self._here_data.traffic_mode: - if self._here_data.traffic_time is not None: - return str(round(self._here_data.traffic_time / 60)) + if self._here_data.traffic_mode and self._here_data.traffic_time is not None: + return str(round(self._here_data.traffic_time / 60)) if self._here_data.base_time is not None: return str(round(self._here_data.base_time / 60)) @@ -271,9 +271,9 @@ class HERETravelTimeSensor(Entity): return self._name @property - def device_state_attributes( + def extra_state_attributes( self, - ) -> Optional[Dict[str, Union[None, float, str, bool]]]: + ) -> dict[str, None | float | str | bool] | None: """Return the state attributes.""" if self._here_data.base_time is None: return None @@ -324,7 +324,7 @@ class HERETravelTimeSensor(Entity): await self.hass.async_add_executor_job(self._here_data.update) - async def _get_location_from_entity(self, entity_id: str) -> Optional[str]: + async def _get_location_from_entity(self, entity_id: str) -> str | None: """Get the location from the entity state or attributes.""" entity = self.hass.states.get(entity_id) @@ -457,11 +457,9 @@ class HERETravelTimeData: _LOGGER.debug("Raw response is: %s", response.response) - # pylint: disable=no-member source_attribution = response.response.get("sourceAttribution") if source_attribution is not None: self.attribution = self._build_hass_attribution(source_attribution) - # pylint: disable=no-member route = response.response["route"] summary = route[0]["summary"] waypoint = route[0]["waypoint"] @@ -477,13 +475,12 @@ class HERETravelTimeData: else: # Convert to kilometers self.distance = distance / 1000 - # pylint: disable=no-member self.route = response.route_short self.origin_name = waypoint[0]["mappedRoadName"] self.destination_name = waypoint[1]["mappedRoadName"] @staticmethod - def _build_hass_attribution(source_attribution: Dict) -> Optional[str]: + def _build_hass_attribution(source_attribution: dict) -> str | None: """Build a hass frontend ready string out of the sourceAttribution.""" suppliers = source_attribution.get("supplier") if suppliers is not None: diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index 90c4b6ce8b9..0d57278c826 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -252,7 +252,7 @@ class HikvisionBinarySensor(BinarySensorEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attr = {ATTR_LAST_TRIP_TIME: self._sensor_last_update()} diff --git a/homeassistant/components/hisense_aehw4a1/translations/hu.json b/homeassistant/components/hisense_aehw4a1/translations/hu.json index 0b21d7c4c32..c71972772eb 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/hu.json +++ b/homeassistant/components/hisense_aehw4a1/translations/hu.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 Hisense AEH-W4A1 eszk\u00f6z.", - "single_instance_allowed": "Csak egy konfigur\u00e1ci\u00f3 lehet Hisense AEH-W4A1 eset\u00e9n." + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "confirm": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/id.json b/homeassistant/components/hisense_aehw4a1/translations/id.json new file mode 100644 index 00000000000..eb31eb979c4 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan Hisense AEH-W4A1?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/ko.json b/homeassistant/components/hisense_aehw4a1/translations/ko.json index 491887280c0..dadb6cc7948 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ko.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/nl.json b/homeassistant/components/hisense_aehw4a1/translations/nl.json index 14f2445f63e..c1f353558b6 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/nl.json +++ b/homeassistant/components/hisense_aehw4a1/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Geen Hisense AEH-W4A1-apparaten gevonden op het netwerk.", - "single_instance_allowed": "Slechts een enkele configuratie van Hisense AEH-W4A1 is mogelijk." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 1e22e45a892..09f459b32d6 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,11 +1,13 @@ """Provide pre-made queries on top of the recorder component.""" +from __future__ import annotations + from collections import defaultdict from datetime import datetime as dt, timedelta from itertools import groupby import json import logging import time -from typing import Iterable, Optional, cast +from typing import Iterable, cast from aiohttp import web from sqlalchemy import and_, bindparam, func, not_, or_ @@ -27,13 +29,12 @@ from homeassistant.const import ( CONF_INCLUDE, HTTP_BAD_REQUEST, ) -from homeassistant.core import Context, State, split_entity_id +from homeassistant.core import Context, HomeAssistant, State, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, ) -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -71,6 +72,7 @@ IGNORE_DOMAINS = ("zone", "scene") NEED_ATTRIBUTE_DOMAINS = { "climate", "humidifier", + "input_datetime", "thermostat", "water_heater", } @@ -461,7 +463,7 @@ class HistoryPeriodView(HomeAssistantView): self.use_include_order = use_include_order async def get( - self, request: web.Request, datetime: Optional[str] = None + self, request: web.Request, datetime: str | None = None ) -> web.Response: """Return history over a period of time.""" datetime_ = None @@ -670,7 +672,7 @@ def _glob_to_like(glob_str): def _entities_may_have_state_changes_after( - hass: HomeAssistantType, entity_ids: Iterable, start_time: dt + hass: HomeAssistant, entity_ids: Iterable, start_time: dt ) -> bool: """Check the state machine to see if entities have changed since start time.""" for entity_id in entity_ids: @@ -713,7 +715,7 @@ class LazyState(State): self._attributes = json.loads(self._row.attributes) except ValueError: # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self) + _LOGGER.exception("Error converting row to state: %s", self._row) self._attributes = {} return self._attributes diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 6778e893f6f..b8d3dc39187 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -6,7 +6,7 @@ import math import voluptuous as vol from homeassistant.components import history -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_ENTITY_ID, CONF_NAME, @@ -19,7 +19,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import setup_reload_service import homeassistant.util.dt as dt_util @@ -102,7 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class HistoryStatsSensor(Entity): +class HistoryStatsSensor(SensorEntity): """Representation of a HistoryStats sensor.""" def __init__( @@ -174,7 +173,7 @@ class HistoryStatsSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self.value is None: return {} diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index ace6540fe71..cbd6b7eeff8 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -74,14 +74,13 @@ class HitronCODADeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the device with the given MAC address.""" - name = next( + return next( (result.name for result in self.last_results if result.mac == device), None ) - return name def _login(self): """Log in to the router. This is required for subsequent api calls.""" - _LOGGER.info("Logging in to CODA...") + _LOGGER.info("Logging in to CODA") try: data = [("user", self._username), (self._type, self._password)] @@ -101,12 +100,11 @@ class HitronCODADeviceScanner(DeviceScanner): def _update_info(self): """Get ARP from router.""" - _LOGGER.info("Fetching...") + _LOGGER.info("Fetching") - if self._userid is None: - if not self._login(): - _LOGGER.error("Could not obtain a user ID from the router") - return False + if self._userid is None and not self._login(): + _LOGGER.error("Could not obtain a user ID from the router") + return False last_results = [] # doing a request diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 331ab37224f..040ef7b4674 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,43 +1,26 @@ """Support for the Hive devices and services.""" +import asyncio from functools import wraps import logging -from pyhiveapi import Hive +from aiohttp.web_exceptions import HTTPException +from apyhiveapi import Hive +from apyhiveapi.helper.hive_exceptions import HiveReauthRequired import voluptuous as vol -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS -ATTR_AVAILABLE = "available" -DOMAIN = "hive" -DATA_HIVE = "data_hive" -SERVICES = ["Heating", "HotWater", "TRV"] -SERVICE_BOOST_HOT_WATER = "boost_hot_water" -SERVICE_BOOST_HEATING = "boost_heating" -ATTR_TIME_PERIOD = "time_period" -ATTR_MODE = "on_off" -DEVICETYPES = { - "binary_sensor": "device_list_binary_sensor", - "climate": "device_list_climate", - "water_heater": "device_list_water_heater", - "light": "device_list_light", - "switch": "device_list_plug", - "sensor": "device_list_sensor", -} +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -52,101 +35,88 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -BOOST_HEATING_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - 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), - } -) - -BOOST_HOT_WATER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( - cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 - ), - vol.Required(ATTR_MODE): cv.string, - } -) - async def async_setup(hass, config): - """Set up the Hive Component.""" + """Hive configuration setup.""" + hass.data[DOMAIN] = {} - async def heating_boost(service): - """Handle the service call.""" + if DOMAIN not in config: + return True - entity_lookup = hass.data[DOMAIN]["entity_lookup"] - hive_id = entity_lookup.get(service.data[ATTR_ENTITY_ID]) - if not hive_id: - # log or raise error - _LOGGER.error("Cannot boost entity id entered") - return + conf = config[DOMAIN] - minutes = service.data[ATTR_TIME_PERIOD] - temperature = service.data[ATTR_TEMPERATURE] + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_USERNAME: conf[CONF_USERNAME], + CONF_PASSWORD: conf[CONF_PASSWORD], + }, + ) + ) + return True - hive.heating.turn_boost_on(hive_id, minutes, temperature) - async def hot_water_boost(service): - """Handle the service call.""" - entity_lookup = hass.data[DOMAIN]["entity_lookup"] - hive_id = entity_lookup.get(service.data[ATTR_ENTITY_ID]) - if not hive_id: - # log or raise error - _LOGGER.error("Cannot boost entity id entered") - return - minutes = service.data[ATTR_TIME_PERIOD] - mode = service.data[ATTR_MODE] +async def async_setup_entry(hass, entry): + """Set up Hive from a config entry.""" - if mode == "on": - hive.hotwater.turn_boost_on(hive_id, minutes) - elif mode == "off": - hive.hotwater.turn_boost_off(hive_id) + websession = aiohttp_client.async_get_clientsession(hass) + hive = Hive(websession) + hive_config = dict(entry.data) - hive = Hive() + hive_config["options"] = {} + hive_config["options"].update( + {CONF_SCAN_INTERVAL: dict(entry.options).get(CONF_SCAN_INTERVAL, 120)} + ) + hass.data[DOMAIN][entry.entry_id] = hive - config = {} - config["username"] = config[DOMAIN][CONF_USERNAME] - config["password"] = config[DOMAIN][CONF_PASSWORD] - config["update_interval"] = config[DOMAIN][CONF_SCAN_INTERVAL] - - devices = await hive.session.startSession(config) - - if devices is None: - _LOGGER.error("Hive API initialization failed") + try: + devices = await hive.session.startSession(hive_config) + except HTTPException as error: + _LOGGER.error("Could not connect to the internet: %s", error) + raise ConfigEntryNotReady() from error + 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 - hass.data[DOMAIN][DATA_HIVE] = hive - hass.data[DOMAIN]["entity_lookup"] = {} - - for ha_type in DEVICETYPES: - devicelist = devices.get(DEVICETYPES[ha_type]) - if devicelist: + for ha_type, hive_type in PLATFORM_LOOKUP.items(): + device_list = devices.get(hive_type) + if device_list: hass.async_create_task( - async_load_platform(hass, ha_type, DOMAIN, devicelist, config) + hass.config_entries.async_forward_entry_setup(entry, ha_type) ) - if ha_type == "climate": - hass.services.async_register( - DOMAIN, - SERVICE_BOOST_HEATING, - heating_boost, - schema=BOOST_HEATING_SCHEMA, - ) - if ha_type == "water_heater": - hass.services.async_register( - DOMAIN, - SERVICE_BOOST_HOT_WATER, - hot_water_boost, - schema=BOOST_HOT_WATER_SCHEMA, - ) return True +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 + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + def refresh_system(func): """Force update all entities after state change.""" @@ -173,6 +143,3 @@ class HiveEntity(Entity): self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) ) - if self.device["hiveType"] in SERVICES: - entity_lookup = self.hass.data[DOMAIN]["entity_lookup"] - entity_lookup[self.entity_id] = self.device["hiveID"] diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 41f1dacc8f3..d5f1ca53afd 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -10,7 +10,8 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) -from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity +from . import HiveEntity +from .const import ATTR_MODE, DOMAIN DEVICETYPE = { "contactsensor": DEVICE_CLASS_OPENING, @@ -24,13 +25,11 @@ PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Hive Binary Sensor.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN].get(DATA_HIVE) - devices = hive.devices.get("binary_sensor") + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("binary_sensor") entities = [] if devices: for dev in devices: @@ -49,7 +48,14 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): @property def device_info(self): """Return device information.""" - return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} + return { + "identifiers": {(DOMAIN, self.device["device_id"])}, + "name": self.device["device_name"], + "model": self.device["deviceData"]["model"], + "manufacturer": self.device["deviceData"]["manufacturer"], + "sw_version": self.device["deviceData"]["version"], + "via_device": (DOMAIN, self.device["parentDevice"]), + } @property def device_class(self): @@ -69,10 +75,9 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): return True @property - def device_state_attributes(self): + def extra_state_attributes(self): """Show Device Attributes.""" return { - ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE), ATTR_MODE: self.attributes.get(ATTR_MODE), } @@ -84,5 +89,5 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.sensor.get_sensor(self.device) + self.device = await self.hive.sensor.getSensor(self.device) self.attributes = self.device.get("attributes", {}) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index f1901147f17..31b4bd273ad 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,6 +1,8 @@ """Support for the Hive climate devices.""" from datetime import timedelta +import voluptuous as vol + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -15,8 +17,10 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers import config_validation as cv, entity_platform -from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import HiveEntity, refresh_system +from .const import ATTR_TIME_PERIOD, DOMAIN, SERVICE_BOOST_HEATING HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -45,19 +49,32 @@ PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Hive thermostat.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN].get(DATA_HIVE) - devices = hive.devices.get("climate") + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("climate") entities = [] if devices: for dev in devices: entities.append(HiveClimateEntity(hive, dev)) async_add_entities(entities, True) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_BOOST_HEATING, + { + 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", + ) + class HiveClimateEntity(HiveEntity, ClimateEntity): """Hive Climate Device.""" @@ -76,7 +93,14 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): @property def device_info(self): """Return device information.""" - return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} + return { + "identifiers": {(DOMAIN, self.device["device_id"])}, + "name": self.device["device_name"], + "model": self.device["deviceData"]["model"], + "manufacturer": self.device["deviceData"]["manufacturer"], + "sw_version": self.device["deviceData"]["version"], + "via_device": (DOMAIN, self.device["parentDevice"]), + } @property def supported_features(self): @@ -93,11 +117,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): """Return if the device is available.""" return self.device["deviceData"]["online"] - @property - def device_state_attributes(self): - """Show Device Attributes.""" - return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)} - @property def hvac_modes(self): """Return the list of available hvac operation modes. @@ -160,27 +179,31 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" new_mode = HASS_TO_HIVE_STATE[hvac_mode] - await self.hive.heating.set_mode(self.device, new_mode) + await self.hive.heating.setMode(self.device, new_mode) @refresh_system async def async_set_temperature(self, **kwargs): """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: - await self.hive.heating.set_target_temperature(self.device, new_temperature) + await self.hive.heating.setTargetTemperature(self.device, new_temperature) @refresh_system 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.turn_boost_off(self.device) + await self.hive.heating.turnBoostOff(self.device) elif preset_mode == PRESET_BOOST: curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - await self.hive.heating.turn_boost_on(self.device, 30, temperature) + await self.hive.heating.turnBoostOn(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) async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.heating.get_heating(self.device) - self.attributes.update(self.device.get("attributes", {})) + self.device = await self.hive.heating.getHeating(self.device) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py new file mode 100644 index 00000000000..b00ba57a96e --- /dev/null +++ b/homeassistant/components/hive/config_flow.py @@ -0,0 +1,167 @@ +"""Config Flow for Hive.""" + +from apyhiveapi import Auth +from apyhiveapi.helper.hive_exceptions import ( + HiveApiError, + HiveInvalid2FACode, + HiveInvalidPassword, + HiveInvalidUsername, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.core import callback + +from .const import CONF_CODE, CONFIG_ENTRY_VERSION, DOMAIN + + +class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Hive config flow.""" + + VERSION = CONFIG_ENTRY_VERSION + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the config flow.""" + self.hive_auth = None + self.data = {} + self.tokens = {} + self.entry = None + + async def async_step_user(self, user_input=None): + """Prompt user input. Create or edit entry.""" + errors = {} + # Login to Hive with user data. + if user_input is not None: + self.data.update(user_input) + self.hive_auth = Auth( + username=self.data[CONF_USERNAME], password=self.data[CONF_PASSWORD] + ) + + # Get user from existing entry and abort if already setup + self.entry = await self.async_set_unique_id(self.data[CONF_USERNAME]) + if self.context["source"] != config_entries.SOURCE_REAUTH: + self._abort_if_unique_id_configured() + + # Login to the Hive. + try: + self.tokens = await self.hive_auth.login() + except HiveInvalidUsername: + errors["base"] = "invalid_username" + except HiveInvalidPassword: + errors["base"] = "invalid_password" + except HiveApiError: + errors["base"] = "no_internet_available" + + if self.tokens.get("ChallengeName") == "SMS_MFA": + # Complete SMS 2FA. + return await self.async_step_2fa() + + if not errors: + # Complete the entry setup. + try: + return await self.async_setup_hive_entry() + except UnknownHiveError: + errors["base"] = "unknown" + + # Show User Input form. + schema = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_2fa(self, user_input=None): + """Handle 2fa step.""" + errors = {} + + if user_input and user_input["2fa"] == "0000": + self.tokens = await self.hive_auth.login() + elif user_input: + try: + self.tokens = await self.hive_auth.sms_2fa( + user_input["2fa"], self.tokens + ) + except HiveInvalid2FACode: + errors["base"] = "invalid_code" + except HiveApiError: + errors["base"] = "no_internet_available" + + if not errors: + try: + return await self.async_setup_hive_entry() + except UnknownHiveError: + errors["base"] = "unknown" + + schema = vol.Schema({vol.Required(CONF_CODE): str}) + return self.async_show_form(step_id="2fa", data_schema=schema, errors=errors) + + async def async_setup_hive_entry(self): + """Finish setup and create the config entry.""" + + if "AuthenticationResult" not in self.tokens: + raise UnknownHiveError + + # Setup the config entry + self.data["tokens"] = self.tokens + if self.context["source"] == config_entries.SOURCE_REAUTH: + self.hass.config_entries.async_update_entry( + self.entry, title=self.data["username"], data=self.data + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self.data["username"], data=self.data) + + async def async_step_reauth(self, user_input=None): + """Re Authenticate a user.""" + data = { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + return await self.async_step_user(data) + + async def async_step_import(self, user_input=None): + """Import user.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Hive options callback.""" + return HiveOptionsFlowHandler(config_entry) + + +class HiveOptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options for Hive.""" + + def __init__(self, config_entry): + """Initialize Hive options flow.""" + self.hive = None + self.config_entry = config_entry + self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + self.hive = self.hass.data["hive"][self.config_entry.entry_id] + errors = {} + if user_input is not None: + new_interval = user_input.get(CONF_SCAN_INTERVAL) + await self.hive.updateInterval(new_interval) + return self.async_create_entry(title="", data=user_input) + + schema = vol.Schema( + { + vol.Optional(CONF_SCAN_INTERVAL, default=self.interval): vol.All( + vol.Coerce(int), vol.Range(min=30) + ) + } + ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + +class UnknownHiveError(Exception): + """Catch unknown hive error.""" diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py new file mode 100644 index 00000000000..ea416fbfe32 --- /dev/null +++ b/homeassistant/components/hive/const.py @@ -0,0 +1,20 @@ +"""Constants for Hive.""" +ATTR_MODE = "mode" +ATTR_TIME_PERIOD = "time_period" +ATTR_ONOFF = "on_off" +CONF_CODE = "2fa" +CONFIG_ENTRY_VERSION = 1 +DEFAULT_NAME = "Hive" +DOMAIN = "hive" +PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch", "water_heater"] +PLATFORM_LOOKUP = { + "binary_sensor": "binary_sensor", + "climate": "climate", + "light": "light", + "sensor": "sensor", + "switch": "switch", + "water_heater": "water_heater", +} +SERVICE_BOOST_HOT_WATER = "boost_hot_water" +SERVICE_BOOST_HEATING = "boost_heating" +WATER_HEATER_MODES = ["on", "off"] diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index f458c27d019..46e8c5b5790 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -12,19 +12,18 @@ from homeassistant.components.light import ( ) import homeassistant.util.color as color_util -from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import HiveEntity, refresh_system +from .const import ATTR_MODE, DOMAIN PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Hive Light.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN].get(DATA_HIVE) - devices = hive.devices.get("light") + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("light") entities = [] if devices: for dev in devices: @@ -43,7 +42,14 @@ class HiveDeviceLight(HiveEntity, LightEntity): @property def device_info(self): """Return device information.""" - return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} + return { + "identifiers": {(DOMAIN, self.device["device_id"])}, + "name": self.device["device_name"], + "model": self.device["deviceData"]["model"], + "manufacturer": self.device["deviceData"]["manufacturer"], + "sw_version": self.device["deviceData"]["version"], + "via_device": (DOMAIN, self.device["parentDevice"]), + } @property def name(self): @@ -56,10 +62,9 @@ class HiveDeviceLight(HiveEntity, LightEntity): return self.device["deviceData"]["online"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Show Device Attributes.""" return { - ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE), ATTR_MODE: self.attributes.get(ATTR_MODE), } @@ -117,14 +122,14 @@ class HiveDeviceLight(HiveEntity, LightEntity): saturation = int(get_new_color[1]) new_color = (hue, saturation, 100) - await self.hive.light.turn_on( + await self.hive.light.turnOn( self.device, new_brightness, new_color_temp, new_color ) @refresh_system async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - await self.hive.light.turn_off(self.device) + await self.hive.light.turnOff(self.device) @property def supported_features(self): @@ -142,5 +147,5 @@ class HiveDeviceLight(HiveEntity, LightEntity): async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.light.get_light(self.device) + self.device = await self.hive.light.getLight(self.device) self.attributes.update(self.device.get("attributes", {})) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 27f235949bf..f8f40401599 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -1,9 +1,10 @@ { "domain": "hive", "name": "Hive", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ - "pyhiveapi==0.3.4.4" + "pyhiveapi==0.3.9" ], "codeowners": [ "@Rendili", diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index e828dff9b4e..518f3286231 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,10 +2,10 @@ from datetime import timedelta -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity -from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity +from . import HiveEntity +from .const import DOMAIN PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) @@ -14,22 +14,19 @@ DEVICETYPE = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Hive Sensor.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN].get(DATA_HIVE) - devices = hive.devices.get("sensor") + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("sensor") entities = [] if devices: for dev in devices: - if dev["hiveType"] in DEVICETYPE: - entities.append(HiveSensorEntity(hive, dev)) + entities.append(HiveSensorEntity(hive, dev)) async_add_entities(entities, True) -class HiveSensorEntity(HiveEntity, Entity): +class HiveSensorEntity(HiveEntity, SensorEntity): """Hive Sensor Entity.""" @property @@ -40,7 +37,14 @@ class HiveSensorEntity(HiveEntity, Entity): @property def device_info(self): """Return device information.""" - return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} + return { + "identifiers": {(DOMAIN, self.device["device_id"])}, + "name": self.device["device_name"], + "model": self.device["deviceData"]["model"], + "manufacturer": self.device["deviceData"]["manufacturer"], + "sw_version": self.device["deviceData"]["version"], + "via_device": (DOMAIN, self.device["parentDevice"]), + } @property def available(self): @@ -67,12 +71,7 @@ class HiveSensorEntity(HiveEntity, Entity): """Return the state of the sensor.""" return self.device["status"]["state"] - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)} - async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) - self.device = await self.hive.sensor.get_sensor(self.device) + self.device = await self.hive.sensor.getSensor(self.device) diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index f09baea7655..f029af7b0b5 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -1,24 +1,62 @@ boost_heating: + name: Boost Heating description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. fields: entity_id: - description: Enter the entity_id for the device required to set the boost mode. - example: "climate.heating" + name: Entity ID + description: Select entity_id to boost. + required: true + example: climate.heating + selector: + entity: + integration: hive + domain: climate time_period: + name: Time Period description: Set the time period for the boost. - example: "01:30:00" + required: true + example: 01:30:00 + selector: + time: temperature: + name: Temperature description: Set the target temperature for the boost period. - example: "20.5" + required: true + example: 20.5 + selector: + number: + min: 7 + max: 35 + step: 0.5 + unit_of_measurement: degrees + mode: slider boost_hot_water: - description: "Set the boost mode ON or OFF defining the period of time for the boost." + name: Boost Hotwater + description: Set the boost mode ON or OFF defining the period of time for the boost. fields: entity_id: - description: Enter the entity_id for the device reuired to set the boost mode. - example: "water_heater.hot_water" + name: Entity ID + description: Select entity_id to boost. + required: true + example: water_heater.hot_water + selector: + entity: + integration: hive + domain: water_heater time_period: + name: Time Period description: Set the time period for the boost. - example: "01:30:00" + required: true + example: 01:30:00 + selector: + time: on_off: + name: Mode description: Set the boost function on or off. + required: true example: "on" + selector: + select: + options: + - "on" + - "off" diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json new file mode 100644 index 00000000000..0a7a587b2db --- /dev/null +++ b/homeassistant/components/hive/strings.json @@ -0,0 +1,53 @@ +{ + "config": { + "step": { + "user": { + "title": "Hive Login", + "description": "Enter your Hive login information and configuration.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "scan_interval": "Scan Interval (seconds)" + } + }, + "2fa": { + "title": "Hive Two-factor Authentication.", + "description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.", + "data": { + "2fa": "Two-factor code" + } + }, + "reauth": { + "title": "Hive Login", + "description": "Re-enter your Hive login information.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_username": "Failed to sign into Hive. Your email address is not recognised.", + "invalid_password": "Failed to sign into Hive. Incorrect password please try again.", + "invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.", + "no_internet_available": "An internet connection is required to connect to Hive.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "unknown_entry": "Unable to find existing entry.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "user": { + "title": "Options for Hive", + "description": "Update the scan interval to poll for data more often.", + "data": { + "scan_interval": "Scan Interval (seconds)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 8ab820589cf..acc2040db00 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -3,19 +3,18 @@ from datetime import timedelta from homeassistant.components.switch import SwitchEntity -from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import HiveEntity, refresh_system +from .const import ATTR_MODE, DOMAIN PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Hive Switch.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN].get(DATA_HIVE) - devices = hive.devices.get("switch") + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("switch") entities = [] if devices: for dev in devices: @@ -34,7 +33,15 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): @property def device_info(self): """Return device information.""" - return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} + if self.device["hiveType"] == "activeplug": + return { + "identifiers": {(DOMAIN, self.device["device_id"])}, + "name": self.device["device_name"], + "model": self.device["deviceData"]["model"], + "manufacturer": self.device["deviceData"]["manufacturer"], + "sw_version": self.device["deviceData"]["version"], + "via_device": (DOMAIN, self.device["parentDevice"]), + } @property def name(self): @@ -47,10 +54,9 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): return self.device["deviceData"].get("online") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Show Device Attributes.""" return { - ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE), ATTR_MODE: self.attributes.get(ATTR_MODE), } @@ -67,16 +73,14 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): @refresh_system async def async_turn_on(self, **kwargs): """Turn the switch on.""" - if self.device["hiveType"] == "activeplug": - await self.hive.switch.turn_on(self.device) + await self.hive.switch.turnOn(self.device) @refresh_system async def async_turn_off(self, **kwargs): """Turn the device off.""" - if self.device["hiveType"] == "activeplug": - await self.hive.switch.turn_off(self.device) + await self.hive.switch.turnOff(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.switch.get_plug(self.device) + self.device = await self.hive.switch.getPlug(self.device) diff --git a/homeassistant/components/hive/translations/ca.json b/homeassistant/components/hive/translations/ca.json new file mode 100644 index 00000000000..eacccda82e7 --- /dev/null +++ b/homeassistant/components/hive/translations/ca.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unknown_entry": "No s'ha pogut trobar l'entrada existent." + }, + "error": { + "invalid_code": "No s'ha pogut iniciar sessi\u00f3 a Hive. El codi de verificaci\u00f3 en dos passos no \u00e9s correcte.", + "invalid_password": "No s'ha pogut iniciar sessi\u00f3 a Hive. Contrasenya incorrecta, torna-ho a provar.", + "invalid_username": "No s'ha pogut iniciar sessi\u00f3 a Hive. L'adre\u00e7a de correu electr\u00f2nic no s'ha reconegut.", + "no_internet_available": "Cal una connexi\u00f3 a Internet per connectar-se al Hive.", + "unknown": "Error inesperat" + }, + "step": { + "2fa": { + "data": { + "2fa": "Codi de verificaci\u00f3 en dos passos" + }, + "description": "Introdueix codi d'autenticaci\u00f3 Hive. \n\n Introdueix el codi 0000 per demanar un altre codi.", + "title": "Verificaci\u00f3 en dos passos de Hive." + }, + "reauth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Torna a introduir la informaci\u00f3 d'inici de sessi\u00f3 del Hive.", + "title": "Inici de sessi\u00f3 Hive" + }, + "user": { + "data": { + "password": "Contrasenya", + "scan_interval": "Interval d'escaneig (segons)", + "username": "Nom d'usuari" + }, + "description": "Actualitza la informaci\u00f3 i configuraci\u00f3 d'inici de sessi\u00f3.", + "title": "Inici de sessi\u00f3 Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Interval d'escaneig (segons)" + }, + "description": "Actualitza l'interval d'escaneig per sondejar les dades m\u00e9s sovint.", + "title": "Opcions de Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/cs.json b/homeassistant/components/hive/translations/cs.json new file mode 100644 index 00000000000..8544a3de7b8 --- /dev/null +++ b/homeassistant/components/hive/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/de.json b/homeassistant/components/hive/translations/de.json new file mode 100644 index 00000000000..bd5876bb023 --- /dev/null +++ b/homeassistant/components/hive/translations/de.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unknown_entry": "Vorhandener Eintrag kann nicht gefunden werden." + }, + "error": { + "invalid_code": "Anmeldung bei Hive fehlgeschlagen. Dein Zwei-Faktor-Authentifizierungscode war falsch.", + "invalid_password": "Anmeldung bei Hive fehlgeschlagen. Falsches Passwort, bitte versuche es erneut.", + "invalid_username": "Die Anmeldung bei Hive ist fehlgeschlagen. Deine E-Mail-Adresse wird nicht erkannt.", + "no_internet_available": "F\u00fcr die Verbindung mit Hive ist eine Internetverbindung erforderlich.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "2fa": { + "data": { + "2fa": "Zwei-Faktor Authentifizierungscode" + }, + "description": "Gib deinen Hive-Authentifizierungscode ein. \n \nBitte gib den Code 0000 ein, um einen anderen Code anzufordern.", + "title": "Hive Zwei-Faktor-Authentifizierung." + }, + "reauth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Gebe deine Hive Anmeldeinformationen erneut ein.", + "title": "Hive Anmeldung" + }, + "user": { + "data": { + "password": "Passwort", + "scan_interval": "Scanintervall (Sekunden)", + "username": "Benutzername" + }, + "description": "Gebe deine Anmeldeinformationen und -konfiguration f\u00fcr Hive ein", + "title": "Hive Anmeldung" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Scanintervall (Sekunden)" + }, + "description": "Aktualisiere den Scanintervall, um Daten \u00f6fters abzufragen.", + "title": "Optionen f\u00fcr Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/el.json b/homeassistant/components/hive/translations/el.json new file mode 100644 index 00000000000..986dd52ef19 --- /dev/null +++ b/homeassistant/components/hive/translations/el.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", + "reauth_successful": "H \u03b5\u03c0\u03b1\u03bd\u03b1\u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2.", + "unknown_entry": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2." + }, + "error": { + "invalid_code": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Hive. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03ae\u03c4\u03b1\u03bd \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2.", + "invalid_password": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Hive. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "invalid_username": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Hive. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", + "no_internet_available": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u0394\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03b3\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Hive.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Hive. \n\n \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc 0000 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc.", + "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 Hive." + }, + "reauth": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Hive.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 Hive" + }, + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03a3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Hive.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03a3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" + }, + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c0\u03b9\u03bf \u03c3\u03c5\u03c7\u03bd\u03ac.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/en.json b/homeassistant/components/hive/translations/en.json new file mode 100644 index 00000000000..32453da0a0c --- /dev/null +++ b/homeassistant/components/hive/translations/en.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful", + "unknown_entry": "Unable to find existing entry." + }, + "error": { + "invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.", + "invalid_password": "Failed to sign into Hive. Incorrect password please try again.", + "invalid_username": "Failed to sign into Hive. Your email address is not recognised.", + "no_internet_available": "An internet connection is required to connect to Hive.", + "unknown": "Unexpected error" + }, + "step": { + "2fa": { + "data": { + "2fa": "Two-factor code" + }, + "description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.", + "title": "Hive Two-factor Authentication." + }, + "reauth": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Re-enter your Hive login information.", + "title": "Hive Login" + }, + "user": { + "data": { + "password": "Password", + "scan_interval": "Scan Interval (seconds)", + "username": "Username" + }, + "description": "Enter your Hive login information and configuration.", + "title": "Hive Login" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Scan Interval (seconds)" + }, + "description": "Update the scan interval to poll for data more often.", + "title": "Options for Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json new file mode 100644 index 00000000000..eb5ef0fd6eb --- /dev/null +++ b/homeassistant/components/hive/translations/es.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dos factores" + }, + "description": "Introduzca su c\u00f3digo de autentificaci\u00f3n Hive. \n \n Introduzca el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", + "title": "Autenticaci\u00f3n de dos factores de Hive." + }, + "reauth": { + "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)" + }, + "description": "Ingrese su configuraci\u00f3n e informaci\u00f3n de inicio de sesi\u00f3n de Hive.", + "title": "Inicio de sesi\u00f3n en Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Intervalo de exploraci\u00f3n (segundos)" + }, + "description": "Actualice el intervalo de escaneo para buscar datos m\u00e1s a menudo.", + "title": "Opciones para Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/et.json b/homeassistant/components/hive/translations/et.json new file mode 100644 index 00000000000..5cffcb036d3 --- /dev/null +++ b/homeassistant/components/hive/translations/et.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unknown_entry": "Olemasolevat kirjet ei leitud." + }, + "error": { + "invalid_code": "Hive sisselogimine nurjus. Kaheastmeline autentimiskood oli vale.", + "invalid_password": "Hive sisselogimine nurjus. Vale parool, proovi uuesti.", + "invalid_username": "Hive sisselogimine nurjus. E-posti aadressi ei tuvastatud.", + "no_internet_available": "Hive-ga \u00fchenduse loomiseks on vajalik Interneti\u00fchendus.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kaheastmelise tuvastuse kood" + }, + "description": "Sisesta oma Hive autentimiskood. \n\n Uue koodi taotlemiseks sisesta kood 0000.", + "title": "Hive kaheastmeline autentimine." + }, + "reauth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta oma Hive sisselogimisandmed uuesti.", + "title": "Hive sisselogimine" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "scan_interval": "P\u00e4ringute intervall (sekundites)", + "username": "Kasutajanimi" + }, + "description": "Sisesta oma Hive sisselogimisteave ja s\u00e4tted.", + "title": "Hive sisselogimine" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "P\u00e4ringute intervall (sekundites)" + }, + "description": "Muuda k\u00fcsitlemise intervalli p\u00e4ringute tihendamiseks.", + "title": "Hive s\u00e4tted" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/fr.json b/homeassistant/components/hive/translations/fr.json new file mode 100644 index 00000000000..5868a2bf175 --- /dev/null +++ b/homeassistant/components/hive/translations/fr.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "unknown_entry": "Impossible de trouver l'entr\u00e9e existante." + }, + "error": { + "invalid_code": "\u00c9chec de la connexion \u00e0 Hive. Votre code d'authentification \u00e0 deux facteurs \u00e9tait incorrect.", + "invalid_password": "\u00c9chec de la connexion \u00e0 Hive. Mot de passe incorrect, veuillez r\u00e9essayer.", + "invalid_username": "\u00c9chec de la connexion \u00e0 Hive. Votre adresse e-mail n'est pas reconnue.", + "no_internet_available": "Une connexion Internet est requise pour se connecter \u00e0 Hive.", + "unknown": "Erreur inattendue" + }, + "step": { + "2fa": { + "data": { + "2fa": "Code \u00e0 deux facteurs" + }, + "description": "Entrez votre code d\u2019authentification Hive. \n \nVeuillez entrer le code 0000 pour demander un autre code.", + "title": "Authentification \u00e0 deux facteurs Hive." + }, + "reauth": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Entrez \u00e0 nouveau vos informations de connexion Hive.", + "title": "Connexion \u00e0 Hive" + }, + "user": { + "data": { + "password": "Mot de passe", + "scan_interval": "Intervalle de scan (secondes)", + "username": "Nom d'utilisateur" + }, + "description": "Entrez vos informations de connexion et votre configuration Hive.", + "title": "Connexion \u00e0 Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Intervalle de scan (secondes)" + }, + "description": "Mettez \u00e0 jour l\u2019intervalle d\u2019analyse pour obtenir des donn\u00e9es plus souvent.", + "title": "Options pour Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json new file mode 100644 index 00000000000..80c6a7e40f1 --- /dev/null +++ b/homeassistant/components/hive/translations/hu.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "unknown_entry": "Nem tal\u00e1lhat\u00f3 megl\u00e9v\u0151 bejegyz\u00e9s." + }, + "error": { + "invalid_code": "Nem siker\u00fclt bejelentkezni a Hive-ba. A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d helytelen volt.", + "invalid_password": "Nem siker\u00fclt bejelentkezni a Hive-ba. Helytelen jelsz\u00f3, pr\u00f3b\u00e1lkozz \u00fajra.", + "invalid_username": "Nem siker\u00fclt bejelentkezni a Hive-ba. Az email c\u00edmedet nem siker\u00fclt felismerni.", + "no_internet_available": "A Hive-hoz val\u00f3 csatlakoz\u00e1shoz internetkapcsolat sz\u00fcks\u00e9ges.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "2fa": { + "data": { + "2fa": "K\u00e9tfaktoros k\u00f3d" + }, + "description": "Add meg a Hive hiteles\u00edt\u00e9si k\u00f3dj\u00e1t. \n \n\u00cdrd be a 0000 k\u00f3dot m\u00e1sik k\u00f3d k\u00e9r\u00e9s\u00e9hez.", + "title": "Hive k\u00e9tfaktoros hiteles\u00edt\u00e9s." + }, + "reauth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Add meg \u00fajra a Hive bejelentkez\u00e9si adatait.", + "title": "Hive Bejelentkez\u00e9s" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Add meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.", + "title": "Hive Bejelentkez\u00e9s" + } + } + }, + "options": { + "step": { + "user": { + "title": "Hive be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/it.json b/homeassistant/components/hive/translations/it.json new file mode 100644 index 00000000000..fd79ca35b79 --- /dev/null +++ b/homeassistant/components/hive/translations/it.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown_entry": "Impossibile trovare la voce esistente." + }, + "error": { + "invalid_code": "Impossibile accedere a Hive. Il codice di autenticazione a due fattori non era corretto.", + "invalid_password": "Impossibile accedere a Hive. Password errata, riprova.", + "invalid_username": "Impossibile accedere a Hive. Il tuo indirizzo email non \u00e8 riconosciuto.", + "no_internet_available": "\u00c8 necessaria una connessione Internet per connettersi a Hive.", + "unknown": "Errore imprevisto" + }, + "step": { + "2fa": { + "data": { + "2fa": "Codice a due fattori" + }, + "description": "Inserisci il tuo codice di autenticazione Hive. \n\n Inserisci il codice 0000 per richiedere un altro codice.", + "title": "Autenticazione a due fattori di Hive." + }, + "reauth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci nuovamente le tue informazioni di accesso a Hive.", + "title": "Accesso Hive" + }, + "user": { + "data": { + "password": "Password", + "scan_interval": "Intervallo di scansione (secondi)", + "username": "Nome utente" + }, + "description": "Immettere le informazioni di accesso e la configurazione di Hive.", + "title": "Accesso Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Intervallo di scansione (secondi)" + }, + "description": "Aggiorna l'intervallo di scansione per eseguire la verifica ciclica dei dati pi\u00f9 spesso.", + "title": "Opzioni per Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/ko.json b/homeassistant/components/hive/translations/ko.json new file mode 100644 index 00000000000..1f06a2f1ac7 --- /dev/null +++ b/homeassistant/components/hive/translations/ko.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "unknown_entry": "\uae30\uc874 \ud56d\ubaa9\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_code": "Hive\uc5d0 \ub85c\uadf8\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. 2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "invalid_password": "Hive\uc5d0 \ub85c\uadf8\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ube44\ubc00\ubc88\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_username": "Hive\uc5d0 \ub85c\uadf8\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \uc778\uc2dd\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "no_internet_available": "Hive\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc778\ud130\ub137 \uc5f0\uacb0\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "2fa": { + "data": { + "2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc" + }, + "description": "Hive \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n\ub2e4\ub978 \ucf54\ub4dc\ub97c \uc694\uccad\ud558\ub824\uba74 \ucf54\ub4dc 0000\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Hive 2\ub2e8\uacc4 \uc778\uc99d." + }, + "reauth": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Hive \ub85c\uadf8\uc778 \uc815\ubcf4\ub97c \ub2e4\uc2dc \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Hive \ub85c\uadf8\uc778" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Hive \ub85c\uadf8\uc778 \uc815\ubcf4 \ubc0f \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Hive \ub85c\uadf8\uc778" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" + }, + "description": "\ub370\uc774\ud130\ub97c \ub354 \uc790\uc8fc \ud3f4\ub9c1\ud558\ub824\uba74 \uac80\uc0c9 \uac04\uaca9\uc744 \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694.", + "title": "Hive \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/nl.json b/homeassistant/components/hive/translations/nl.json new file mode 100644 index 00000000000..3ac45ae14d7 --- /dev/null +++ b/homeassistant/components/hive/translations/nl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol", + "unknown_entry": "Kan bestaand item niet vinden." + }, + "error": { + "invalid_code": "Aanmelden bij Hive is mislukt. Uw tweefactorauthenticatiecode was onjuist.", + "invalid_password": "Aanmelden bij Hive is mislukt. Onjuist wachtwoord, probeer het opnieuw.", + "invalid_username": "Aanmelden bij Hive is mislukt. Uw e-mailadres wordt niet herkend.", + "no_internet_available": "Een internetverbinding is vereist om verbinding te maken met Hive.", + "unknown": "Onverwachte fout" + }, + "step": { + "2fa": { + "data": { + "2fa": "Tweefactorauthenticatiecode" + }, + "description": "Voer uw Hive-verificatiecode in. \n \n Voer code 0000 in om een andere code aan te vragen.", + "title": "Hive tweefactorauthenticatie" + }, + "reauth": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer uw Hive-aanmeldingsgegevens opnieuw in.", + "title": "Hive-aanmelding" + }, + "user": { + "data": { + "password": "Wachtwoord", + "scan_interval": "Scaninterval (seconden)", + "username": "Gebruikersnaam" + }, + "description": "Voer uw Hive login informatie en configuratie in.", + "title": "Hive-aanmelding" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Scaninterval (seconden)" + }, + "description": "Werk het scaninterval bij om vaker naar gegevens te vragen.", + "title": "Opties voor Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/no.json b/homeassistant/components/hive/translations/no.json new file mode 100644 index 00000000000..c5213aafeee --- /dev/null +++ b/homeassistant/components/hive/translations/no.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unknown_entry": "Kunne ikke finne eksisterende oppf\u00f8ring." + }, + "error": { + "invalid_code": "Kunne ikke logge p\u00e5 Hive. Tofaktorautentiseringskoden din var feil.", + "invalid_password": "Kunne ikke logge p\u00e5 Hive. Feil passord. Vennligst pr\u00f8v igjen.", + "invalid_username": "Kunne ikke logge p\u00e5 Hive. E-postadressen din blir ikke gjenkjent.", + "no_internet_available": "Det kreves en internettforbindelse for \u00e5 koble til Hive.", + "unknown": "Uventet feil" + }, + "step": { + "2fa": { + "data": { + "2fa": "Totrinnsbekreftelse kode" + }, + "description": "Skriv inn din Hive-godkjenningskode. \n\n Vennligst skriv inn kode 0000 for \u00e5 be om en annen kode.", + "title": "Hive Totrinnsbekreftelse autentisering." + }, + "reauth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Skriv innloggingsinformasjonen for Hive p\u00e5 nytt.", + "title": "Hive-p\u00e5logging" + }, + "user": { + "data": { + "password": "Passord", + "scan_interval": "Skanneintervall (sekunder)", + "username": "Brukernavn" + }, + "description": "Skriv inn inn innloggingsinformasjonen og konfigurasjonen for Hive.", + "title": "Hive-p\u00e5logging" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Skanneintervall (sekunder)" + }, + "description": "Oppdater skanneintervallet for \u00e5 avstemme etter data oftere.", + "title": "Alternativer for Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/pl.json b/homeassistant/components/hive/translations/pl.json new file mode 100644 index 00000000000..0c61fa74feb --- /dev/null +++ b/homeassistant/components/hive/translations/pl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unknown_entry": "Nie mo\u017cna znale\u017a\u0107 istniej\u0105cego wpisu." + }, + "error": { + "invalid_code": "Nie uda\u0142o si\u0119 zalogowa\u0107 do Hive. Tw\u00f3j kod uwierzytelniania dwusk\u0142adnikowego by\u0142 nieprawid\u0142owy.", + "invalid_password": "Nie uda\u0142o si\u0119 zalogowa\u0107 do Hive. Nieprawid\u0142owe has\u0142o, spr\u00f3buj ponownie.", + "invalid_username": "Nie uda\u0142o si\u0119 zalogowa\u0107 do Hive. Tw\u00f3j adres e-mail nie zosta\u0142 rozpoznany.", + "no_internet_available": "Do po\u0142\u0105czenia z Hive wymagane jest po\u0142\u0105czenie z internetem.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" + }, + "description": "Wprowad\u017a sw\u00f3j kod uwierzytelniaj\u0105cy Hive. \n\nWprowad\u017a kod 0000, aby poprosi\u0107 o kolejny kod.", + "title": "Uwierzytelnianie dwusk\u0142adnikowe Hive" + }, + "reauth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a ponownie swoje dane logowania do Hive.", + "title": "Login Hive" + }, + "user": { + "data": { + "password": "Has\u0142o", + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane logowania i konfiguracj\u0119 Hive.", + "title": "Login Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)" + }, + "description": "Zaktualizuj cz\u0119stotliwo\u015b\u0107 skanowania, aby cz\u0119\u015bciej sondowa\u0107 dane.", + "title": "Opcje Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/pt.json b/homeassistant/components/hive/translations/pt.json new file mode 100644 index 00000000000..8397b5cb0a4 --- /dev/null +++ b/homeassistant/components/hive/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "reauth": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/ru.json b/homeassistant/components/hive/translations/ru.json new file mode 100644 index 00000000000..02736871d24 --- /dev/null +++ b/homeassistant/components/hive/translations/ru.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "unknown_entry": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c." + }, + "error": { + "invalid_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Hive. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_password": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Hive. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "invalid_username": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Hive. \u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", + "no_internet_available": "\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Hive \u0438\u043b\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 0000, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u0434.", + "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "reauth": { + "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 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432 Hive.", + "title": "Hive" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 Hive.", + "title": "Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u0447\u0430\u0449\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435, \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430.", + "title": "Hive" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/zh-Hant.json b/homeassistant/components/hive/translations/zh-Hant.json new file mode 100644 index 00000000000..0af7e218f6e --- /dev/null +++ b/homeassistant/components/hive/translations/zh-Hant.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unknown_entry": "\u7121\u6cd5\u627e\u5230\u73fe\u6709\u5be6\u9ad4\u3002" + }, + "error": { + "invalid_code": "Hive \u767b\u5165\u5931\u6557\u3002\u96d9\u91cd\u8a8d\u8b49\u78bc\u4e0d\u6b63\u78ba\u3002", + "invalid_password": "Hive \u767b\u5165\u5931\u6557\u3002\u5bc6\u78bc\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_username": "Hive \u767b\u5165\u5931\u6557\u3002\u627e\u4e0d\u5230\u96fb\u5b50\u90f5\u4ef6\u3002", + "no_internet_available": "\u9700\u8981\u7db2\u969b\u7db2\u8def\u9023\u7dda\u4ee5\u9023\u7dda\u81f3 Hive\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u96d9\u91cd\u8a8d\u8b49\u78bc" + }, + "description": "\u8f38\u5165 Hive \u8a8d\u8b49\u78bc\u3002\n \n \u8acb\u8f38\u5165 0000 \u4ee5\u7372\u53d6\u5176\u4ed6\u8a8d\u8b49\u78bc\u3002", + "title": "\u96d9\u91cd\u8a8d\u8b49" + }, + "reauth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u91cd\u65b0\u8f38\u5165 Hive \u767b\u5165\u8cc7\u8a0a\u3002", + "title": "Hive \u767b\u5165\u8cc7\u8a0a" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165 Hive \u767b\u5165\u8cc7\u8a0a\u8207\u8a2d\u5b9a\u3002", + "title": "Hive \u767b\u5165\u8cc7\u8a0a" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09" + }, + "description": "\u66f4\u65b0\u6383\u63cf\u9593\u8ddd\u4ee5\u66f4\u983b\u7e41\u7372\u53d6\u66f4\u65b0\u3002", + "title": "Hive \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 56e98a690b8..5d8eb590ea7 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -2,6 +2,8 @@ from datetime import timedelta +import voluptuous as vol + from homeassistant.components.water_heater import ( STATE_ECO, STATE_OFF, @@ -10,8 +12,16 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, ) from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers import config_validation as cv, entity_platform -from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system +from . import HiveEntity, refresh_system +from .const import ( + ATTR_ONOFF, + ATTR_TIME_PERIOD, + DOMAIN, + SERVICE_BOOST_HOT_WATER, + WATER_HEATER_MODES, +) SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE HOTWATER_NAME = "Hot Water" @@ -32,19 +42,32 @@ HASS_TO_HIVE_STATE = { SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Hive Hotwater.""" - if discovery_info is None: - return +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN].get(DATA_HIVE) - devices = hive.devices.get("water_heater") + hive = hass.data[DOMAIN][entry.entry_id] + devices = hive.session.deviceList.get("water_heater") entities = [] if devices: for dev in devices: entities.append(HiveWaterHeater(hive, dev)) async_add_entities(entities, True) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_BOOST_HOT_WATER, + { + vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( + cv.time_period, + cv.positive_timedelta, + lambda td: td.total_seconds() // 60, + ), + vol.Required(ATTR_ONOFF): vol.In(WATER_HEATER_MODES), + }, + "async_hot_water_boost", + ) + class HiveWaterHeater(HiveEntity, WaterHeaterEntity): """Hive Water Heater Device.""" @@ -57,7 +80,14 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): @property def device_info(self): """Return device information.""" - return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} + return { + "identifiers": {(DOMAIN, self.device["device_id"])}, + "name": self.device["device_name"], + "model": self.device["deviceData"]["model"], + "manufacturer": self.device["deviceData"]["manufacturer"], + "sw_version": self.device["deviceData"]["version"], + "via_device": (DOMAIN, self.device["parentDevice"]), + } @property def supported_features(self): @@ -92,20 +122,28 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): @refresh_system async def async_turn_on(self, **kwargs): """Turn on hotwater.""" - await self.hive.hotwater.set_mode(self.device, "MANUAL") + await self.hive.hotwater.setMode(self.device, "MANUAL") @refresh_system async def async_turn_off(self, **kwargs): """Turn on hotwater.""" - await self.hive.hotwater.set_mode(self.device, "OFF") + await self.hive.hotwater.setMode(self.device, "OFF") @refresh_system async def async_set_operation_mode(self, operation_mode): """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] - await self.hive.hotwater.set_mode(self.device, new_mode) + await self.hive.hotwater.setMode(self.device, new_mode) + + @refresh_system + async def async_hot_water_boost(self, time_period, on_off): + """Handle the service call.""" + if on_off == "on": + await self.hive.hotwater.turnBoostOn(self.device, time_period) + elif on_off == "off": + await self.hive.hotwater.turnBoostOff(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.hotwater.get_hotwater(self.device) + self.device = await self.hive.hotwater.getHotwater(self.device) diff --git a/homeassistant/components/hlk_sw16/translations/hu.json b/homeassistant/components/hlk_sw16/translations/hu.json index 3b2d79a34a7..0abcc301f0c 100644 --- a/homeassistant/components/hlk_sw16/translations/hu.json +++ b/homeassistant/components/hlk_sw16/translations/hu.json @@ -2,6 +2,20 @@ "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" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/id.json b/homeassistant/components/hlk_sw16/translations/id.json new file mode 100644 index 00000000000..ed8fde32106 --- /dev/null +++ b/homeassistant/components/hlk_sw16/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", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/ru.json b/homeassistant/components/hlk_sw16/translations/ru.json index 9e0db9fcf94..9b02cafd466 100644 --- a/homeassistant/components/hlk_sw16/translations/ru.json +++ b/homeassistant/components/hlk_sw16/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 301bd1976e6..baf4fd17f85 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -71,9 +71,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await update_all_devices(hass, entry) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -84,8 +84,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 064ae033fb0..463de6cda51 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_ENTITIES, DEVICE_CLASS_TIMESTAMP import homeassistant.util.dt as dt_util @@ -27,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(await hass.async_add_executor_job(get_entities), True) -class HomeConnectSensor(HomeConnectEntity): +class HomeConnectSensor(HomeConnectEntity, SensorEntity): """Sensor class for Home Connect.""" def __init__(self, device, desc, key, unit, icon, device_class, sign=1): diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json index f02fb97b9df..aa43f65b520 100644 --- a/homeassistant/components/home_connect/translations/hu.json +++ b/homeassistant/components/home_connect/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." + }, "create_entry": { - "default": "Sikeres autentik\u00e1ci\u00f3" + "default": "Sikeres hiteles\u00edt\u00e9s" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/home_connect/translations/id.json b/homeassistant/components/home_connect/translations/id.json new file mode 100644 index 00000000000..bc6089beb2b --- /dev/null +++ b/homeassistant/components/home_connect/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "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})" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py new file mode 100644 index 00000000000..e559cd030b3 --- /dev/null +++ b/homeassistant/components/home_plus_control/__init__.py @@ -0,0 +1,179 @@ +"""The Legrand Home+ Control integration.""" +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from homepluscontrol.homeplusapi import HomePlusControlApiError +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.helpers import ( + config_entry_oauth2_flow, + config_validation as cv, + dispatcher, +) +from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from . import config_flow, helpers +from .api import HomePlusControlAsyncApi +from .const import ( + API, + CONF_SUBSCRIPTION_KEY, + DATA_COORDINATOR, + DISPATCHER_REMOVERS, + DOMAIN, + ENTITY_UIDS, + SIGNAL_ADD_ENTITIES, +) + +# Configuration schema for component in configuration.yaml +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Required(CONF_SUBSCRIPTION_KEY): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +# The Legrand Home+ Control platform is currently limited to "switch" entities +PLATFORMS = ["switch"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the Legrand Home+ Control component from configuration.yaml.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + # Register the implementation from the config information + config_flow.HomePlusControlFlowHandler.async_register_implementation( + hass, + helpers.HomePlusControlOAuth2Implementation(hass, config[DOMAIN]), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up Legrand Home+ Control from a config entry.""" + hass_entry_data = hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) + + # Retrieve the registered implementation + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, config_entry + ) + ) + + # Using an aiohttp-based API lib, so rely on async framework + # Add the API object to the domain's data in HA + api = hass_entry_data[API] = HomePlusControlAsyncApi( + hass, config_entry, implementation + ) + + # Set of entity unique identifiers of this integration + uids = hass_entry_data[ENTITY_UIDS] = set() + + # Integration dispatchers + hass_entry_data[DISPATCHER_REMOVERS] = [] + + device_registry = async_get_device_registry(hass) + + # Register the Data Coordinator with the integration + async def async_update_data(): + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + module_data = await api.async_get_modules() + except HomePlusControlApiError as err: + raise UpdateFailed( + f"Error communicating with API: {err} [{type(err)}]" + ) from err + + # Remove obsolete entities from Home Assistant + entity_uids_to_remove = uids - set(module_data) + for uid in entity_uids_to_remove: + uids.remove(uid) + device = device_registry.async_get_device({(DOMAIN, uid)}) + device_registry.async_remove_device(device.id) + + # Send out signal for new entity addition to Home Assistant + new_entity_uids = set(module_data) - uids + if new_entity_uids: + uids.update(new_entity_uids) + dispatcher.async_dispatcher_send( + hass, + SIGNAL_ADD_ENTITIES, + new_entity_uids, + coordinator, + ) + + return module_data + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="home_plus_control_module", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=60), + ) + hass_entry_data[DATA_COORDINATOR] = coordinator + + async def start_platforms(): + """Continue setting up the platforms.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(config_entry, platform) + for platform in PLATFORMS + ] + ) + # Only refresh the coordinator after all platforms are loaded. + await coordinator.async_refresh() + + hass.async_create_task(start_platforms()) + + return True + + +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 + ] + ) + ) + if unload_ok: + # Unsubscribe the config_entry signal dispatcher connections + dispatcher_removers = hass.data[DOMAIN][config_entry.entry_id].pop( + "dispatcher_removers" + ) + for remover in dispatcher_removers: + remover() + + # And finally unload the domain config entry data + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/home_plus_control/api.py b/homeassistant/components/home_plus_control/api.py new file mode 100644 index 00000000000..d9db95323de --- /dev/null +++ b/homeassistant/components/home_plus_control/api.py @@ -0,0 +1,55 @@ +"""API for Legrand Home+ Control bound to Home Assistant OAuth.""" +from homepluscontrol.homeplusapi import HomePlusControlAPI + +from homeassistant import config_entries, core +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow + +from .const import DEFAULT_UPDATE_INTERVALS + + +class HomePlusControlAsyncApi(HomePlusControlAPI): + """Legrand Home+ Control object that interacts with the OAuth2-based API of the provider. + + This API is bound the HomeAssistant Config Entry that corresponds to this component. + + Attributes:. + hass (HomeAssistant): HomeAssistant core object. + config_entry (ConfigEntry): ConfigEntry object that configures this API. + implementation (AbstractOAuth2Implementation): OAuth2 implementation that handles AA and + token refresh. + _oauth_session (OAuth2Session): OAuth2Session object within implementation. + """ + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ) -> None: + """Initialize the HomePlusControlAsyncApi object. + + Initialize the authenticated API for the Legrand Home+ Control component. + + Args:. + hass (HomeAssistant): HomeAssistant core object. + config_entry (ConfigEntry): ConfigEntry object that configures this API. + implementation (AbstractOAuth2Implementation): OAuth2 implementation that handles AA + and token refresh. + """ + self._oauth_session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + + # Create the API authenticated client - external library + super().__init__( + subscription_key=implementation.subscription_key, + oauth_client=aiohttp_client.async_get_clientsession(hass), + update_intervals=DEFAULT_UPDATE_INTERVALS, + ) + + async def async_get_access_token(self) -> str: + """Return a valid access token.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token["access_token"] diff --git a/homeassistant/components/home_plus_control/config_flow.py b/homeassistant/components/home_plus_control/config_flow.py new file mode 100644 index 00000000000..ed1686f7af1 --- /dev/null +++ b/homeassistant/components/home_plus_control/config_flow.py @@ -0,0 +1,32 @@ +"""Config flow for Legrand Home+ Control.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + + +class HomePlusControlFlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Home+ Control OAuth2 authentication.""" + + DOMAIN = DOMAIN + + # Pick the Cloud Poll class + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + async def async_step_user(self, user_input=None): + """Handle a flow start initiated by the user.""" + await self.async_set_unique_id(DOMAIN) + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await super().async_step_user(user_input) diff --git a/homeassistant/components/home_plus_control/const.py b/homeassistant/components/home_plus_control/const.py new file mode 100644 index 00000000000..0ebae0bef20 --- /dev/null +++ b/homeassistant/components/home_plus_control/const.py @@ -0,0 +1,45 @@ +"""Constants for the Legrand Home+ Control integration.""" +API = "api" +CONF_SUBSCRIPTION_KEY = "subscription_key" +CONF_PLANT_UPDATE_INTERVAL = "plant_update_interval" +CONF_PLANT_TOPOLOGY_UPDATE_INTERVAL = "plant_topology_update_interval" +CONF_MODULE_STATUS_UPDATE_INTERVAL = "module_status_update_interval" + +DATA_COORDINATOR = "coordinator" +DOMAIN = "home_plus_control" +ENTITY_UIDS = "entity_unique_ids" +DISPATCHER_REMOVERS = "dispatcher_removers" + +# Legrand Model Identifiers - https://developer.legrand.com/documentation/product-cluster-list/# +HW_TYPE = { + "NLC": "NLC - Cable Outlet", + "NLF": "NLF - On-Off Dimmer Switch w/o Neutral", + "NLP": "NLP - Socket (Connected) Outlet", + "NLPM": "NLPM - Mobile Socket Outlet", + "NLM": "NLM - Micromodule Switch", + "NLV": "NLV - Shutter Switch with Neutral", + "NLLV": "NLLV - Shutter Switch with Level Control", + "NLL": "NLL - On-Off Toggle Switch with Neutral", + "NLT": "NLT - Remote Switch", + "NLD": "NLD - Double Gangs On-Off Remote Switch", +} + +# Legrand OAuth2 URIs +OAUTH2_AUTHORIZE = "https://partners-login.eliotbylegrand.com/authorize" +OAUTH2_TOKEN = "https://partners-login.eliotbylegrand.com/token" + +# The Legrand Home+ Control API has very limited request quotas - at the time of writing, it is +# limited to 500 calls per day (resets at 00:00) - so we want to keep updates to a minimum. +DEFAULT_UPDATE_INTERVALS = { + # Seconds between API checks for plant information updates. This is expected to change very + # little over time because a user's plants (homes) should rarely change. + CONF_PLANT_UPDATE_INTERVAL: 7200, # 120 minutes + # Seconds between API checks for plant topology updates. This is expected to change little + # over time because the modules in the user's plant should be relatively stable. + CONF_PLANT_TOPOLOGY_UPDATE_INTERVAL: 3600, # 60 minutes + # Seconds between API checks for module status updates. This can change frequently so we + # check often + CONF_MODULE_STATUS_UPDATE_INTERVAL: 300, # 5 minutes +} + +SIGNAL_ADD_ENTITIES = "home_plus_control_add_entities_signal" diff --git a/homeassistant/components/home_plus_control/helpers.py b/homeassistant/components/home_plus_control/helpers.py new file mode 100644 index 00000000000..95d538def01 --- /dev/null +++ b/homeassistant/components/home_plus_control/helpers.py @@ -0,0 +1,53 @@ +"""Helper classes and functions for the Legrand Home+ Control integration.""" +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import CONF_SUBSCRIPTION_KEY, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +class HomePlusControlOAuth2Implementation( + config_entry_oauth2_flow.LocalOAuth2Implementation +): + """OAuth2 implementation that extends the HomeAssistant local implementation. + + It provides the name of the integration and adds support for the subscription key. + + Attributes: + hass (HomeAssistant): HomeAssistant core object. + client_id (str): Client identifier assigned by the API provider when registering an app. + client_secret (str): Client secret assigned by the API provider when registering an app. + subscription_key (str): Subscription key obtained from the API provider. + authorize_url (str): Authorization URL initiate authentication flow. + token_url (str): URL to retrieve access/refresh tokens. + name (str): Name of the implementation (appears in the HomeAssitant GUI). + """ + + def __init__( + self, + hass: HomeAssistant, + config_data: dict, + ): + """HomePlusControlOAuth2Implementation Constructor. + + Initialize the authentication implementation for the Legrand Home+ Control API. + + Args: + hass (HomeAssistant): HomeAssistant core object. + config_data (dict): Configuration data that complies with the config Schema + of this component. + """ + super().__init__( + hass=hass, + domain=DOMAIN, + client_id=config_data[CONF_CLIENT_ID], + client_secret=config_data[CONF_CLIENT_SECRET], + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) + self.subscription_key = config_data[CONF_SUBSCRIPTION_KEY] + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Home+ Control" diff --git a/homeassistant/components/home_plus_control/manifest.json b/homeassistant/components/home_plus_control/manifest.json new file mode 100644 index 00000000000..1eb143ca3c2 --- /dev/null +++ b/homeassistant/components/home_plus_control/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "home_plus_control", + "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" + ] +} diff --git a/homeassistant/components/home_plus_control/strings.json b/homeassistant/components/home_plus_control/strings.json new file mode 100644 index 00000000000..c991c9e0279 --- /dev/null +++ b/homeassistant/components/home_plus_control/strings.json @@ -0,0 +1,21 @@ +{ + "title": "Legrand Home+ Control", + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py new file mode 100644 index 00000000000..d4167ae1f9e --- /dev/null +++ b/homeassistant/components/home_plus_control/switch.py @@ -0,0 +1,129 @@ +"""Legrand Home+ Control Switch Entity Module that uses the HomeAssistant DataUpdateCoordinator.""" +from functools import partial + +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + DEVICE_CLASS_SWITCH, + SwitchEntity, +) +from homeassistant.core import callback +from homeassistant.helpers import dispatcher +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DISPATCHER_REMOVERS, DOMAIN, HW_TYPE, SIGNAL_ADD_ENTITIES + + +@callback +def add_switch_entities(new_unique_ids, coordinator, add_entities): + """Add switch entities to the platform. + + Args: + new_unique_ids (set): Unique identifiers of entities to be added to Home Assistant. + coordinator (DataUpdateCoordinator): Data coordinator of this platform. + add_entities (function): Method called to add entities to Home Assistant. + """ + new_entities = [] + for uid in new_unique_ids: + new_ent = HomeControlSwitchEntity(coordinator, uid) + new_entities.append(new_ent) + add_entities(new_entities) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Legrand Home+ Control Switch platform in HomeAssistant. + + Args: + hass (HomeAssistant): HomeAssistant core object. + config_entry (ConfigEntry): ConfigEntry object that configures this platform. + async_add_entities (function): Function called to add entities of this platform. + """ + partial_add_switch_entities = partial( + add_switch_entities, add_entities=async_add_entities + ) + # Connect the dispatcher for the switch platform + hass.data[DOMAIN][config_entry.entry_id][DISPATCHER_REMOVERS].append( + dispatcher.async_dispatcher_connect( + hass, SIGNAL_ADD_ENTITIES, partial_add_switch_entities + ) + ) + + +class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): + """Entity that represents a Legrand Home+ Control switch. + + It extends the HomeAssistant-provided classes of the CoordinatorEntity and the SwitchEntity. + + The CoordinatorEntity class provides: + should_poll + async_update + async_added_to_hass + + The SwitchEntity class provides the functionality of a ToggleEntity and additional power + consumption methods and state attributes. + """ + + def __init__(self, coordinator, idx): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self.idx = idx + self.module = self.coordinator.data[self.idx] + + @property + def name(self): + """Name of the device.""" + return self.module.name + + @property + def unique_id(self): + """ID (unique) of the device.""" + return self.idx + + @property + def device_info(self): + """Device information.""" + return { + "identifiers": { + # Unique identifiers within the domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + "manufacturer": "Legrand", + "model": HW_TYPE.get(self.module.hw_type), + "sw_version": self.module.fw, + } + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + if self.module.device == "plug": + return DEVICE_CLASS_OUTLET + return DEVICE_CLASS_SWITCH + + @property + def available(self) -> bool: + """Return if entity is available. + + This is the case when the coordinator is able to update the data successfully + AND the switch entity is reachable. + + This method overrides the one of the CoordinatorEntity + """ + return self.coordinator.last_update_success and self.module.reachable + + @property + def is_on(self): + """Return entity state.""" + return self.module.status == "on" + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + # Do the turning on. + await self.module.turn_on() + # Update the data + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await self.module.turn_off() + # Update the data + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/home_plus_control/translations/ca.json b/homeassistant/components/home_plus_control/translations/ca.json new file mode 100644 index 00000000000..90e23fcd7ab --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "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.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/cs.json b/homeassistant/components/home_plus_control/translations/cs.json new file mode 100644 index 00000000000..9d7f5156bc3 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/en.json b/homeassistant/components/home_plus_control/translations/en.json new file mode 100644 index 00000000000..f5f8afe73d1 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "authorize_url_timeout": "Timeout generating authorize URL.", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/es-419.json b/homeassistant/components/home_plus_control/translations/es-419.json new file mode 100644 index 00000000000..e35bb529a85 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya ha iniciado", + "authorize_url_timeout": "Se agot\u00f3 el tiempo al generar el URL de autorizaci\u00f3n", + "missing_configuration": "Este componente no est\u00e1 configurado. Por favor sigue la documentaci\u00f3n", + "no_url_available": "Ning\u00fan URL disponible. Para m\u00e1s informaci\u00f3n sobre este error [revisa la secci\u00f3n de ayuda] ({docs_url})", + "single_instance_allowed": "Previamente configurado. S\u00f3lo es posible una configuraci\u00f3n" + }, + "create_entry": { + "default": "Autenticado exitosamente" + }, + "step": { + "pick_implementation": { + "title": "Escoja 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 new file mode 100644 index 00000000000..0046c1f5205 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "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})", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json new file mode 100644 index 00000000000..dbdea8cca56 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "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})" + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "step": { + "pick_implementation": { + "title": "Choisir une m\u00e9thode d'authentification" + } + } + }, + "title": "Legrand Home + Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json new file mode 100644 index 00000000000..2a4775a0b58 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/it.json b/homeassistant/components/home_plus_control/translations/it.json new file mode 100644 index 00000000000..789a7db85eb --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + }, + "title": "Legrand Home + Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ko.json b/homeassistant/components/home_plus_control/translations/ko.json new file mode 100644 index 00000000000..94c8bb91648 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "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.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "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." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + }, + "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 new file mode 100644 index 00000000000..9d448e480a1 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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.", + "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." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/no.json b/homeassistant/components/home_plus_control/translations/no.json new file mode 100644 index 00000000000..363f5cf54f7 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + }, + "title": "Legrand Home + Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/pl.json b/homeassistant/components/home_plus_control/translations/pl.json new file mode 100644 index 00000000000..b684c874a7d --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/pt.json b/homeassistant/components/home_plus_control/translations/pt.json new file mode 100644 index 00000000000..2a1a4a7174d --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ru.json b/homeassistant/components/home_plus_control/translations/ru.json new file mode 100644 index 00000000000..fd3da6929d8 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "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.", + "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.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "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." + }, + "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." + }, + "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" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/zh-Hant.json b/homeassistant/components/home_plus_control/translations/zh-Hant.json new file mode 100644 index 00000000000..0faa3110287 --- /dev/null +++ b/homeassistant/components/home_plus_control/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "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", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + }, + "title": "Legrand Home+ Control" +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index c2ee40b7d43..67eb94a97e7 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -21,15 +21,30 @@ 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.service import async_extract_referenced_entity_ids +from homeassistant.helpers.service import ( + async_extract_config_entry_ids, + async_extract_referenced_entity_ids, +) + +ATTR_ENTRY_ID = "entry_id" _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN SERVICE_RELOAD_CORE_CONFIG = "reload_core_config" +SERVICE_RELOAD_CONFIG_ENTRY = "reload_config_entry" SERVICE_CHECK_CONFIG = "check_config" SERVICE_UPDATE_ENTITY = "update_entity" SERVICE_SET_LOCATION = "set_location" SCHEMA_UPDATE_ENTITY = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}) +SCHEMA_RELOAD_CONFIG_ENTRY = vol.All( + vol.Schema( + { + vol.Optional(ATTR_ENTRY_ID): str, + **cv.ENTITY_SERVICE_FIELDS, + }, + ), + cv.has_at_least_one_key(ATTR_ENTRY_ID, *cv.ENTITY_SERVICE_FIELDS), +) async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: @@ -43,7 +58,8 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: # Generic turn on/off method requires entity id if not all_referenced: _LOGGER.error( - "homeassistant.%s cannot be called without a target", service.service + "The service homeassistant.%s cannot be called without a target", + service.service, ) return @@ -203,4 +219,26 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: vol.Schema({ATTR_LATITUDE: cv.latitude, ATTR_LONGITUDE: cv.longitude}), ) + async def async_handle_reload_config_entry(call): + """Service handler for reloading a config entry.""" + reload_entries = set() + if ATTR_ENTRY_ID in call.data: + reload_entries.add(call.data[ATTR_ENTRY_ID]) + reload_entries.update(await async_extract_config_entry_ids(hass, call)) + if not reload_entries: + raise ValueError("There were no matching config entries to reload") + await asyncio.gather( + *[ + hass.config_entries.async_reload(config_entry_id) + for config_entry_id in reload_entries + ] + ) + + hass.helpers.service.async_register_admin_service( + ha.DOMAIN, + SERVICE_RELOAD_CONFIG_ENTRY, + async_handle_reload_config_entry, + schema=SCHEMA_RELOAD_CONFIG_ENTRY, + ) + return True diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 1ff3915f121..3173d2d8c32 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,7 +1,9 @@ """Allow users to set and activate scenes.""" +from __future__ import annotations + from collections import namedtuple import logging -from typing import Any, List +from typing import Any import voluptuous as vol @@ -118,7 +120,7 @@ _LOGGER = logging.getLogger(__name__) @callback -def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: +def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all scenes that reference the entity.""" if DATA_PLATFORM not in hass.data: return [] @@ -133,7 +135,7 @@ def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: @callback -def entities_in_scene(hass: HomeAssistant, entity_id: str) -> List[str]: +def entities_in_scene(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all entities in a scene.""" if DATA_PLATFORM not in hass.data: return [] @@ -298,7 +300,7 @@ class HomeAssistantScene(Scene): return self.scene_config.id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the scene state attributes.""" attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} unique_id = self.unique_id diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 38814d9f902..251ee171b6a 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -58,3 +58,19 @@ update_entity: description: Force one or more entities to update its data target: entity: {} + +reload_config_entry: + name: Reload config entry + description: Reload a config entry that matches a target. + target: + entity: {} + device: {} + fields: + entry_id: + advanced: true + name: Config entry id + description: A configuration entry id + required: false + example: 8955375327824e14ba89e4b29cc3ec9a + selector: + text: diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json index 194254a0384..8d76ff76b79 100644 --- a/homeassistant/components/homeassistant/translations/fr.json +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -6,7 +6,7 @@ "dev": "D\u00e9veloppement", "docker": "Docker", "docker_version": "Docker", - "hassio": "Superviseur", + "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Type d'installation", "os_name": "Famille du syst\u00e8me d'exploitation", diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json new file mode 100644 index 00000000000..f45b17b1a13 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/he.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "os_name": "\u05de\u05e9\u05e4\u05d7\u05ea \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index e202a747ac7..f6bfe03321e 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -6,13 +6,13 @@ "dev": "Fejleszt\u00e9s", "docker": "Docker", "docker_version": "Docker", - "hassio": "Adminisztr\u00e1tor", + "hassio": "Supervisor", "host_os": "Home Assistant OS", "installation_type": "Telep\u00edt\u00e9s t\u00edpusa", "os_name": "Oper\u00e1ci\u00f3s rendszer csal\u00e1d", "os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja", "python_version": "Python verzi\u00f3", - "supervisor": "Adminisztr\u00e1tor", + "supervisor": "Supervisor", "timezone": "Id\u0151z\u00f3na", "version": "Verzi\u00f3", "virtualenv": "Virtu\u00e1lis k\u00f6rnyezet" diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json new file mode 100644 index 00000000000..2ee86bba815 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/id.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "Arsitektur CPU", + "chassis": "Kerangka", + "dev": "Pengembangan", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "Jenis Instalasi", + "os_name": "Keluarga Sistem Operasi", + "os_version": "Versi Sistem Operasi", + "python_version": "Versi Python", + "supervisor": "Supervisor", + "timezone": "Zona Waktu", + "version": "Versi", + "virtualenv": "Lingkungan Virtual" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index f3168807715..66b8f8a1d14 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -6,13 +6,13 @@ "dev": "Sviluppo", "docker": "Docker", "docker_version": "Docker", - "hassio": "Supervisore", + "hassio": "Supervisor", "host_os": "Sistema Operativo di Home Assistant", "installation_type": "Tipo di installazione", "os_name": "Famiglia del Sistema Operativo", "os_version": "Versione del Sistema Operativo", "python_version": "Versione Python", - "supervisor": "Supervisore", + "supervisor": "Supervisor", "timezone": "Fuso orario", "version": "Versione", "virtualenv": "Ambiente virtuale" diff --git a/homeassistant/components/homeassistant/translations/ko.json b/homeassistant/components/homeassistant/translations/ko.json new file mode 100644 index 00000000000..801d63fd449 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/ko.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "CPU \uc544\ud0a4\ud14d\ucc98", + "chassis": "\uc100\uc2dc", + "dev": "\uac1c\ubc1c\uc790 \ubaa8\ub4dc", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "\uc124\uce58 \uc720\ud615", + "os_name": "\uc6b4\uc601 \uccb4\uc81c \uc81c\ud488\uad70", + "os_version": "\uc6b4\uc601 \uccb4\uc81c \ubc84\uc804", + "python_version": "Python \ubc84\uc804", + "supervisor": "Supervisor", + "timezone": "\uc2dc\uac04\ub300", + "version": "\ubc84\uc804", + "virtualenv": "\uac00\uc0c1 \ud658\uacbd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 47b69068ea3..b4e33dcd7aa 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -11,7 +11,7 @@ "installation_type": "Type installatie", "os_name": "Besturingssysteemfamilie", "os_version": "Versie van het besturingssysteem", - "python_version": "Python versie", + "python_version": "Python-versie", "supervisor": "Supervisor", "timezone": "Tijdzone", "version": "Versie", diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 2bc42c3d063..2e78a93315d 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -31,6 +31,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="event" ): """Listen for events based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None variables = None if automation_info: variables = automation_info.get("variables") @@ -95,6 +96,7 @@ async def async_attach_trigger( "platform": platform_type, "event": event, "description": f"event '{event.event_type}'", + "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 2f3ebfb82d9..2f3ae8e6ad2 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -19,6 +19,7 @@ TRIGGER_SCHEMA = vol.Schema( async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None event = config.get(CONF_EVENT) job = HassJob(action) @@ -34,6 +35,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "platform": "homeassistant", "event": event, "description": "Home Assistant stopping", + "id": trigger_id, } }, event.context, @@ -51,6 +53,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "platform": "homeassistant", "event": event, "description": "Home Assistant starting", + "id": trigger_id, } }, ) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 59f16c41a36..05eed9ee27b 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -78,6 +78,7 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) + trigger_id = automation_info.get("trigger_id") if automation_info else None _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @@ -139,6 +140,7 @@ async def async_attach_trigger( "to_state": to_s, "for": time_delta if not time_delta else period[entity_id], "description": f"numeric state of {entity_id}", + "id": trigger_id, } }, to_s.context, diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 8a03905d98d..69cddbfe126 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -1,7 +1,9 @@ """Offer state listening automation rules.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Dict, Optional +from typing import Any import voluptuous as vol @@ -79,12 +81,13 @@ async def async_attach_trigger( template.attach(hass, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} - period: Dict[str, timedelta] = {} + period: dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) + trigger_id = automation_info.get("trigger_id") if automation_info else None _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @@ -93,8 +96,8 @@ async def async_attach_trigger( def state_automation_listener(event: Event): """Listen for state changes and calls action.""" entity: str = event.data["entity_id"] - from_s: Optional[State] = event.data.get("old_state") - to_s: Optional[State] = event.data.get("new_state") + from_s: State | None = event.data.get("old_state") + to_s: State | None = event.data.get("new_state") if from_s is None: old_value = None @@ -138,6 +141,7 @@ async def async_attach_trigger( "for": time_delta if not time_delta else period[entity], "attribute": attribute, "description": f"state of {entity}", + "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index b0fced4d55d..69f01672078 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -39,6 +39,7 @@ TRIGGER_SCHEMA = vol.Schema( async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None entities = {} removes = [] job = HassJob(action) @@ -54,6 +55,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "now": now, "description": description, "entity_id": entity_id, + "id": trigger_id, } }, ) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 3107d590d91..859f76b773b 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -57,6 +57,7 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) seconds = config.get(CONF_SECONDS) @@ -78,6 +79,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "platform": "time_pattern", "now": now, "description": "time pattern", + "id": trigger_id, } }, ) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 28e2683c259..0e4bcc28aab 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -34,7 +34,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized +from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA @@ -42,7 +42,6 @@ from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.util import get_local_ip -# pylint: disable=unused-import from . import ( # noqa: F401 type_cameras, type_covers, @@ -51,6 +50,7 @@ from . import ( # noqa: F401 type_lights, type_locks, type_media_players, + type_remotes, type_security_systems, type_sensors, type_switches, @@ -59,7 +59,6 @@ from . import ( # noqa: F401 from .accessories import HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( - AID_STORAGE, ATTR_INTERGRATION, ATTR_MANUFACTURER, ATTR_MODEL, @@ -119,6 +118,8 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 +PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 + def _has_all_unique_names_and_ports(bridges): """Validate that each homekit bridge configured has a unique name.""" @@ -132,6 +133,7 @@ def _has_all_unique_names_and_ports(bridges): BRIDGE_SCHEMA = vol.All( cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE), cv.deprecated(CONF_SAFE_MODE), + cv.deprecated(CONF_AUTO_START), vol.Schema( { vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In( @@ -241,9 +243,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): port = conf[CONF_PORT] _LOGGER.debug("Begin setup HomeKit for %s", name) - aid_storage = AccessoryAidStorage(hass, entry.entry_id) - - await aid_storage.async_initialize() # ip_address and advertise_ip are yaml only ip_address = conf.get(CONF_IP_ADDRESS) advertise_ip = conf.get(CONF_ADVERTISE_IP) @@ -276,26 +275,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry.entry_id, entry.title, ) - zeroconf_instance = await zeroconf.async_get_instance(hass) - - # If the previous instance hasn't cleaned up yet - # we need to wait a bit - try: - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) - except (OSError, AttributeError) as ex: - _LOGGER.warning( - "%s could not be setup because the local port %s is in use", name, port - ) - raise ConfigEntryNotReady from ex - - undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { - AID_STORAGE: aid_storage, HOMEKIT: homekit, - UNDO_UPDATE_LISTENER: undo_listener, + UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), } + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + if hass.state == CoreState.running: await homekit.async_start() elif auto_start: @@ -322,12 +309,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if homekit.status == STATUS_RUNNING: await homekit.async_stop() + logged_shutdown_wait = False for _ in range(0, SHUTDOWN_TIMEOUT): - if not await hass.async_add_executor_job( - port_is_available, entry.data[CONF_PORT] - ): + if await hass.async_add_executor_job(port_is_available, entry.data[CONF_PORT]): + break + + if not logged_shutdown_wait: _LOGGER.info("Waiting for the HomeKit server to shutdown") - await asyncio.sleep(1) + logged_shutdown_wait = True + + await asyncio.sleep(PORT_CLEANUP_CHECK_INTERVAL_SECS) hass.data[DOMAIN].pop(entry.entry_id) @@ -463,6 +454,7 @@ class HomeKit: self._entry_id = entry_id self._entry_title = entry_title self._homekit_mode = homekit_mode + self.aid_storage = None self.status = STATUS_READY self.bridge = None @@ -470,7 +462,6 @@ class HomeKit: def setup(self, zeroconf_instance): """Set up bridge and accessory driver.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) ip_addr = self._ip_address or get_local_ip() persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) @@ -503,10 +494,9 @@ class HomeKit: self.driver.config_changed() return - aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE] removed = [] for entity_id in entity_ids: - aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) if aid not in self.bridge.accessories: continue @@ -531,9 +521,6 @@ class HomeKit: def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" - if not self._filter(state.entity_id): - return - # The bridge itself counts as an accessory if len(self.bridge.accessories) + 1 >= MAX_DEVICES: _LOGGER.warning( @@ -550,14 +537,12 @@ class HomeKit: "The bridge %s has entity %s. For best performance, " "and to prevent unexpected unavailability, create and " "pair a separate HomeKit instance in accessory mode for " - "this entity.", + "this entity", self._name, state.entity_id, ) - aid = self.hass.data[DOMAIN][self._entry_id][ - AID_STORAGE - ].get_or_allocate_aid_for_entity_id(state.entity_id) + aid = self.aid_storage.get_or_allocate_aid_for_entity_id(state.entity_id) conf = self._config.pop(state.entity_id, {}) # If an accessory cannot be created or added due to an exception # of any kind (usually in pyhap) it should not prevent @@ -578,15 +563,10 @@ class HomeKit: acc = self.bridge.accessories.pop(aid) return acc - async def async_start(self, *args): - """Start the accessory driver.""" - if self.status != STATUS_READY: - return - self.status = STATUS_WAIT - - ent_reg = await entity_registry.async_get_registry(self.hass) - dev_reg = await device_registry.async_get_registry(self.hass) - + async def async_configure_accessories(self): + """Configure accessories for the included states.""" + dev_reg = device_registry.async_get(self.hass) + ent_reg = entity_registry.async_get(self.hass) device_lookup = ent_reg.async_get_device_class_lookup( { (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING), @@ -597,10 +577,9 @@ class HomeKit: } ) - bridged_states = [] + entity_states = [] for state in self.hass.states.async_all(): entity_id = state.entity_id - if not self._filter(entity_id): continue @@ -611,17 +590,40 @@ class HomeKit: ) self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state) - bridged_states.append(state) + entity_states.append(state) - self._async_register_bridge(dev_reg) - await self._async_start(bridged_states) + return entity_states + + async def async_start(self, *args): + """Load storage and start.""" + if self.status != STATUS_READY: + return + self.status = STATUS_WAIT + zc_instance = await zeroconf.async_get_instance(self.hass) + await self.hass.async_add_executor_job(self.setup, zc_instance) + self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) + await self.aid_storage.async_initialize() + await self._async_create_accessories() + self._async_register_bridge() _LOGGER.debug("Driver start for %s", self._name) await self.driver.async_start() self.status = STATUS_RUNNING + if self.driver.state.paired: + return + + show_setup_message( + self.hass, + self._entry_id, + accessory_friendly_name(self._entry_title, self.driver.accessory), + self.driver.state.pincode, + self.driver.accessory.xhm_uri(), + ) + @callback - def _async_register_bridge(self, dev_reg): + def _async_register_bridge(self): """Register the bridge as a device so homekit_controller and exclude it from discovery.""" + dev_reg = device_registry.async_get(self.hass) formatted_mac = device_registry.format_mac(self.driver.state.mac) # Connections and identifiers are both used here. # @@ -645,8 +647,9 @@ class HomeKit: identifiers={identifier}, connections={connection}, manufacturer=MANUFACTURER, - name=self._name, - model=f"Home Assistant HomeKit {hk_mode_name}", + name=accessory_friendly_name(self._entry_title, self.driver.accessory), + model=f"HomeKit {hk_mode_name}", + entry_type="service", ) @callback @@ -663,14 +666,13 @@ class HomeKit: for device_id in devices_to_purge: dev_reg.async_remove_device(device_id) - async def _async_start(self, entity_states): - """Start the accessory.""" + async def _async_create_accessories(self): + """Create the accessories.""" + entity_states = await self.async_configure_accessories() if self._homekit_mode == HOMEKIT_MODE_ACCESSORY: state = entity_states[0] conf = self._config.pop(state.entity_id, {}) acc = get_accessory(self.hass, self.driver, state, STANDALONE_AID, conf) - - self.driver.add_accessory(acc) else: self.bridge = HomeBridge(self.hass, self.driver, self._name) for state in entity_states: @@ -679,15 +681,6 @@ class HomeKit: await self.hass.async_add_executor_job(self.driver.add_accessory, acc) - if not self.driver.state.paired: - show_setup_message( - self.hass, - self._entry_id, - accessory_friendly_name(self._entry_title, self.driver.accessory), - self.driver.state.pincode, - self.driver.accessory.xhm_uri(), - ) - async def async_stop(self, *args): """Stop the accessory driver.""" if self.status != STATUS_RUNNING: diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 7e68daf4b62..307dbf0e806 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -12,6 +12,7 @@ from homeassistant.components.cover import ( DEVICE_CLASS_WINDOW, ) from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.components.remote import SUPPORT_ACTIVITY from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -22,6 +23,8 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, @@ -54,8 +57,6 @@ from .const import ( CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, EVENT_HOMEKIT_CHANGED, HK_CHARGING, @@ -103,6 +104,7 @@ def get_accessory(hass, driver, state, aid, config): a_type = None name = config.get(CONF_NAME, state.name) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if state.domain == "alarm_control_panel": a_type = "SecuritySystem" @@ -115,7 +117,6 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "cover": device_class = state.attributes.get(ATTR_DEVICE_CLASS) - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE @@ -167,7 +168,7 @@ def get_accessory(hass, driver, state, aid, config): a_type = "AirQualitySensor" elif device_class == DEVICE_CLASS_CO: a_type = "CarbonMonoxideSensor" - elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: + elif device_class == DEVICE_CLASS_CO2 or "co2" in state.entity_id: a_type = "CarbonDioxideSensor" elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", LIGHT_LUX): a_type = "LightSensor" @@ -179,6 +180,9 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "vacuum": a_type = "Vacuum" + elif state.domain == "remote" and features & SUPPORT_ACTIVITY: + a_type = "ActivityRemote" + elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): a_type = "Switch" diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index c21c27fba83..683e533d2df 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -36,13 +37,13 @@ from .const import ( DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, DEFAULT_HOMEKIT_MODE, + DOMAIN, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODE_BRIDGE, HOMEKIT_MODES, SHORT_BRIDGE_NAME, VIDEO_CODEC_COPY, ) -from .const import DOMAIN # pylint:disable=unused-import from .util import async_find_next_available_port, state_needs_accessory_mode CONF_CAMERA_COPY = "camera_copy" @@ -53,7 +54,7 @@ MODE_EXCLUDE = "exclude" INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE] -DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN] +DOMAINS_NEED_ACCESSORY_MODE = [CAMERA_DOMAIN, MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] NEVER_BRIDGED_DOMAINS = [CAMERA_DOMAIN] CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}." @@ -74,7 +75,7 @@ SUPPORTED_DOMAINS = [ "lock", MEDIA_PLAYER_DOMAIN, "person", - "remote", + REMOTE_DOMAIN, "scene", "script", "sensor", @@ -93,6 +94,7 @@ DEFAULT_DOMAINS = [ "light", "lock", MEDIA_PLAYER_DOMAIN, + REMOTE_DOMAIN, "switch", "vacuum", "water_heater", @@ -221,7 +223,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Return a set of bridge names.""" return { entry.data[CONF_NAME] - for entry in self._async_current_entries() + for entry in self._async_current_entries(include_ignore=False) if CONF_NAME in entry.data } @@ -247,10 +249,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Determine is a name or port is already used.""" name = user_input[CONF_NAME] port = user_input[CONF_PORT] - for entry in self._async_current_entries(): - if entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port: - return False - return True + return not any( + entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port + for entry in self._async_current_entries(include_ignore=False) + ) @staticmethod @callback diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 67312903b50..abfc6a2aa38 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -5,7 +5,6 @@ DEBOUNCE_TIMEOUT = 0.5 DEVICE_PRECISION_LEEWAY = 6 DOMAIN = "homekit" HOMEKIT_FILE = ".homekit.state" -AID_STORAGE = "homekit-aid-allocations" HOMEKIT_PAIRING_QR = "homekit-pairing-qr" HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" HOMEKIT = "homekit" @@ -74,8 +73,8 @@ DEFAULT_LOW_BATTERY_THRESHOLD = 20 DEFAULT_MAX_FPS = 30 DEFAULT_MAX_HEIGHT = 1080 DEFAULT_MAX_WIDTH = 1920 -DEFAULT_PORT = 51827 -DEFAULT_CONFIG_FLOW_PORT = 51828 +DEFAULT_PORT = 21063 +DEFAULT_CONFIG_FLOW_PORT = 21064 DEFAULT_SAFE_MODE = False DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264 DEFAULT_VIDEO_MAP = "0:v:0" @@ -238,8 +237,6 @@ PROP_CELSIUS = {"minValue": -273, "maxValue": 999} PROP_VALID_VALUES = "ValidValues" # #### Device Classes #### -DEVICE_CLASS_CO = "co" -DEVICE_CLASS_CO2 = "co2" DEVICE_CLASS_DOOR = "door" DEVICE_CLASS_GARAGE_DOOR = "garage_door" DEVICE_CLASS_GAS = "gas" diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py index 2baede8d957..860d798f113 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/homekit/img_util.py @@ -63,6 +63,6 @@ class TurboJPEGSingleton: TurboJPEGSingleton.__instance = TurboJPEG() except Exception: # pylint: disable=broad-except _LOGGER.exception( - "libturbojpeg is not installed, cameras may impact HomeKit performance" + "Error loading libturbojpeg; Cameras may impact HomeKit performance" ) TurboJPEGSingleton.__instance = False diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index d7ec3297fa4..53438138e43 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==3.4.0", + "HAP-python==3.4.1", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index a9b7c1c6cc1..56bc5438eac 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -18,7 +18,7 @@ "mode": "[%key:common::config_flow::data::mode%]", "entities": "Entities" }, - "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a seperate HomeKit accessory will beeach tv media player and camera.", + "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.", "title": "Select entities to be included" }, "cameras": { diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index dbd83622d8a..1ad6d63a0ff 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 que vulguis 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'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.", "title": "Selecciona les entitats a incloure" }, "init": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 3b0129567c4..aa78c3e4adc 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -4,29 +4,13 @@ "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": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains": "Domains to include", - "mode": "Mode" + "include_domains": "Domains to include" }, "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" @@ -37,8 +21,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" @@ -55,7 +38,7 @@ "entities": "Entities", "mode": "Mode" }, - "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a seperate HomeKit accessory will beeach tv media player and camera.", + "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.", "title": "Select entities to be included" }, "init": { diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 8c24d2d2251..2ae8d651eb1 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, kuvatakse 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 meediumim\u00e4ngija ja kaamera jaoks.", "title": "Vali kaasatavd olemid" }, "init": { diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 4721514e615..dae09002c54 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -55,7 +55,7 @@ "entities": "Entit\u00e9s", "mode": "Mode" }, - "description": "Choisissez les entit\u00e9s \u00e0 exposer. En mode accessoire, une seule entit\u00e9 est expos\u00e9e. En mode d'inclusion de pont, toutes les entit\u00e9s du domaine seront expos\u00e9es \u00e0 moins que des entit\u00e9s sp\u00e9cifiques ne soient s\u00e9lectionn\u00e9es. En mode d'exclusion de pont, toutes les entit\u00e9s du domaine seront expos\u00e9es \u00e0 l'exception des entit\u00e9s exclues.", + "description": "Choisissez les entit\u00e9s \u00e0 inclure. En mode accessoire, une seule entit\u00e9 est incluse. En mode d'inclusion de pont, toutes les entit\u00e9s du domaine seront incluses \u00e0 moins que des entit\u00e9s sp\u00e9cifiques ne soient s\u00e9lectionn\u00e9es. En mode d'exclusion de pont, toutes les entit\u00e9s du domaine seront incluses \u00e0 l'exception des entit\u00e9s exclues. Pour de meilleures performances, un accessoire HomeKit distinct sera cr\u00e9\u00e9 pour chaque lecteur multim\u00e9dia TV et cam\u00e9ra.", "title": "S\u00e9lectionnez les entit\u00e9s \u00e0 exposer" }, "init": { diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json index 6acebca0ca4..cb5a530b739 100644 --- a/homeassistant/components/homekit/translations/he.json +++ b/homeassistant/components/homekit/translations/he.json @@ -4,7 +4,8 @@ "include_exclude": { "data": { "mode": "\u05de\u05e6\u05d1" - } + }, + "title": "\u05d1\u05d7\u05e8 \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05d9\u05d9\u05db\u05dc\u05dc\u05d5" }, "init": { "data": { diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index 4cf1f44c439..7cc2577cb31 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -1,16 +1,52 @@ { + "config": { + "step": { + "accessory_mode": { + "data": { + "entity_id": "Entit\u00e1s" + }, + "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1st" + }, + "pairing": { + "title": "HomeKit p\u00e1ros\u00edt\u00e1s" + }, + "user": { + "data": { + "include_domains": "Felvenni k\u00edv\u00e1nt domainek", + "mode": "M\u00f3d" + }, + "title": "Felvenni k\u00edv\u00e1nt domainek kiv\u00e1laszt\u00e1sa" + } + } + }, "options": { "step": { + "advanced": { + "title": "Halad\u00f3 be\u00e1ll\u00edt\u00e1sok" + }, + "cameras": { + "data": { + "camera_copy": "A nat\u00edv H.264 streameket t\u00e1mogat\u00f3 kamer\u00e1k" + }, + "title": "V\u00e1laszd ki a kamera vide\u00f3 kodekj\u00e9t." + }, "include_exclude": { "data": { "entities": "Entit\u00e1sok", "mode": "M\u00f3d" - } + }, + "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" }, "init": { "data": { + "include_domains": "Felvenni k\u00edv\u00e1nt domainek", "mode": "M\u00f3d" - } + }, + "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt domaineket." + }, + "yaml": { + "description": "Ez a bejegyz\u00e9s YAML-en kereszt\u00fcl vez\u00e9relhet\u0151", + "title": "HomeKit be\u00e1ll\u00edt\u00e1sok m\u00f3dos\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json new file mode 100644 index 00000000000..588631a5215 --- /dev/null +++ b/homeassistant/components/homekit/translations/id.json @@ -0,0 +1,75 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Aksesori atau bridge dengan nama atau port yang sama telah dikonfigurasi." + }, + "step": { + "accessory_mode": { + "data": { + "entity_id": "Entitas" + }, + "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan.", + "title": "Pilih entitas yang akan disertakan" + }, + "bridge_mode": { + "data": { + "include_domains": "Domain yang disertakan" + }, + "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan.", + "title": "Pilih domain yang akan disertakan" + }, + "pairing": { + "description": "Untuk menyelesaikan pemasangan ikuti petunjuk di \"Notifikasi\" di bawah \"Pemasangan HomeKit\".", + "title": "Pasangkan HomeKit" + }, + "user": { + "data": { + "auto_start": "Mulai otomatis (nonaktifkan jika menggunakan Z-Wave atau sistem mulai tertunda lainnya)", + "include_domains": "Domain yang disertakan", + "mode": "Mode" + }, + "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV dan kamera.", + "title": "Pilih domain yang akan disertakan" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)", + "safe_mode": "Mode Aman (aktifkan hanya jika pemasangan gagal)" + }, + "description": "Pengaturan ini hanya perlu disesuaikan jika HomeKit tidak berfungsi.", + "title": "Konfigurasi Tingkat Lanjut" + }, + "cameras": { + "data": { + "camera_copy": "Kamera yang mendukung aliran H.264 asli" + }, + "description": "Periksa semua kamera yang mendukung streaming H.264 asli. Jika kamera tidak mengeluarkan aliran H.264, sistem akan mentranskode video ke H.264 untuk HomeKit. Proses transcoding membutuhkan CPU kinerja tinggi dan tidak mungkin bekerja pada komputer papan tunggal.", + "title": "Pilih codec video kamera." + }, + "include_exclude": { + "data": { + "entities": "Entitas", + "mode": "Mode" + }, + "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan. Dalam mode \"bridge include\", semua entitas di domain akan disertakan, kecuali entitas tertentu dipilih. Dalam mode \"bridge exclude\", semua entitas di domain akan disertakan, kecuali untuk entitas tertentu yang dipilih. Untuk kinerja terbaik, aksesori HomeKit terpisah diperlukan untuk masing-masing pemutar media, TV, dan kamera.", + "title": "Pilih entitas untuk disertakan" + }, + "init": { + "data": { + "include_domains": "Domain yang disertakan", + "mode": "Mode" + }, + "description": "HomeKit dapat dikonfigurasi untuk memaparkakan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.", + "title": "Pilih domain yang akan disertakan." + }, + "yaml": { + "description": "Entri ini dikontrol melalui YAML", + "title": "Sesuaikan Opsi HomeKit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index fee64457652..f92c61d493f 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, ci sar\u00e0 una HomeKit separata in modalit\u00e0 accessorio 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 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 bc8e138fbaf..274898425cb 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -1,16 +1,25 @@ { "config": { "abort": { - "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "port_name_in_use": "\uc774\ub984\uc774\ub098 \ud3ec\ud2b8\uac00 \uac19\uc740 \ube0c\ub9ac\uc9c0 \ub610\ub294 \uc561\uc138\uc11c\ub9ac\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "\uad6c\uc131\uc694\uc18c" + }, + "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.", + "title": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c \uc120\ud0dd\ud558\uae30" + }, "bridge_mode": { "data": { "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" - } + }, + "description": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc9c0\uc6d0\ub418\ub294 \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4.", + "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" }, "pairing": { - "description": "{name} \uc774(\uac00) \uc900\ube44\ub418\uba74 \"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit \ube0c\ub9ac\uc9c0 \uc124\uc815\"\uc73c\ub85c \ud398\uc5b4\ub9c1\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\"\uc54c\ub9bc\"\uc5d0\uc11c \"HomeKit Pairing\"\uc5d0 \uc788\ub294 \uc548\ub0b4\uc5d0 \ub530\ub77c \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", "title": "HomeKit \ud398\uc5b4\ub9c1\ud558\uae30" }, "user": { @@ -19,8 +28,8 @@ "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778", "mode": "\ubaa8\ub4dc" }, - "description": "HomeKit \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \ud1b5\ud574 HomeKit \uc758 Home Assistant \uad6c\uc131\uc694\uc18c\uc5d0 \uc561\uc138\uc2a4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ubaa8\ub4dc\uc5d0\uc11c HomeKit \ube0c\ub9ac\uc9c0\ub294 \ube0c\ub9ac\uc9c0 \uc790\uccb4\ub97c \ud3ec\ud568\ud558\uc5ec \uc778\uc2a4\ud134\uc2a4\ub2f9 150 \uac1c\uc758 \uc561\uc138\uc11c\ub9ac\ub85c \uc81c\ud55c\ub429\ub2c8\ub2e4. \ucd5c\ub300 \uc561\uc138\uc11c\ub9ac \uac1c\uc218\ubcf4\ub2e4 \ub9ce\uc740 \uc218\uc758 \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub824\ub294 \uacbd\uc6b0, \uc11c\ub85c \ub2e4\ub978 \ub3c4\uba54\uc778\uc5d0 \ub300\ud574 \uc5ec\ub7ec\uac1c\uc758 HomeKit \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc758 \uc790\uc138\ud55c \uad6c\uc131\uc740 YAML \uc744 \ud1b5\ud574\uc11c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uacfc \uc608\uae30\uce58 \uc54a\uc740 \uc0ac\uc6a9 \ubd88\uac00\ub2a5\ud55c \uc0c1\ud0dc\ub97c \ubc29\uc9c0\ud558\ub824\uba74 \uac01\uac01\uc758 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\uc5d0 \ub300\ud574 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c \ubcc4\ub3c4\uc758 HomeKit \uc778\uc2a4\ud134\uc2a4\ub97c \uc0dd\uc131\ud558\uace0 \ud398\uc5b4\ub9c1\ud574\uc8fc\uc138\uc694.", - "title": "HomeKit \ud65c\uc131\ud654\ud558\uae30" + "description": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub3c4\uba54\uc778\uc5d0\uc11c \uc9c0\uc6d0\ub418\ub294 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \uce74\uba54\ub77c\uc5d0 \ub300\ud574 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc758 \uac1c\ubcc4 HomeKit \uc778\uc2a4\ud134\uc2a4\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.", + "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" } } }, @@ -31,31 +40,34 @@ "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (homekit.start \uc11c\ube44\uc2a4\ub97c \uc218\ub3d9\uc73c\ub85c \ud638\ucd9c\ud558\ub824\uba74 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" }, - "description": "\uc774 \uc124\uc815\uc740 HomeKit \uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "\uace0\uae09 \uad6c\uc131\ud558\uae30" + "description": "\uc774 \uc124\uc815\uc740 HomeKit\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "\uace0\uae09 \uad6c\uc131" }, "cameras": { "data": { "camera_copy": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \uce74\uba54\ub77c" }, - "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi \uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit\uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi\uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30" }, "include_exclude": { "data": { + "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.", + "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" }, "init": { "data": { "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778", "mode": "\ubaa8\ub4dc" }, - "description": "HomeKit \ub294 \ube0c\ub9ac\uc9c0 \ub610\ub294 \uc561\uc138\uc11c\ub9ac\ub97c \ub178\ucd9c\ud558\ub3c4\ub85d \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. TV \uae30\uae30 \ud074\ub798\uc2a4\uac00 \uc788\ub294 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ud558\ub824\uba74 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc5d0 \ud3ec\ud568\ud558\uac70\ub098 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\ube0c\ub9ac\uc9c0 \ub610\ub294 \ub2e8\uc77c \uc561\uc138\uc11c\ub9ac\ub97c \ub178\ucd9c\ud558\uc5ec HomeKit\ub97c \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. TV \uae30\uae30 \ud074\ub798\uc2a4\uac00 \uc788\ub294 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ud558\ub824\uba74 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\uac00 HomeKit\uc5d0 \ud3ec\ud568\ub418\uba70, \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc5d0 \ud3ec\ud568\ud558\uac70\ub098 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, "yaml": { - "description": "\uc774 \ud56d\ubaa9\uc740 YAML \uc744 \ud1b5\ud574 \uc81c\uc5b4\ub429\ub2c8\ub2e4", + "description": "\uc774 \ud56d\ubaa9\uc740 YAML\uc744 \ud1b5\ud574 \uc81c\uc5b4\ub429\ub2c8\ub2e4", "title": "HomeKit \uc635\uc158 \uc870\uc815\ud558\uae30" } } diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 9013723ac6c..1c65188ee6d 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -7,10 +7,19 @@ "accessory_mode": { "data": { "entity_id": "Entiteit" - } + }, + "description": "Kies de entiteit die moet worden opgenomen. In de accessoiremodus wordt slechts \u00e9\u00e9n entiteit opgenomen.", + "title": "Selecteer de entiteit die u wilt opnemen" + }, + "bridge_mode": { + "data": { + "include_domains": "Domeinen om op te nemen" + }, + "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen.", + "title": "Selecteer domeinen die u wilt opnemen" }, "pairing": { - "description": "Zodra de {name} klaar is, is het koppelen beschikbaar in \"Meldingen\" als \"HomeKit Bridge Setup\".", + "description": "Volg de instructies in \"Meldingen\" onder \"HomeKit-koppeling\" om het koppelen te voltooien.", "title": "Koppel HomeKit" }, "user": { @@ -28,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (uitschakelen bij gebruik van Z-Wave of een ander vertraagd startsysteem)", + "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)", "safe_mode": "Veilige modus (alleen inschakelen als het koppelen mislukt)" }, "description": "Deze instellingen hoeven alleen te worden aangepast als HomeKit niet functioneert.", @@ -45,11 +54,13 @@ "data": { "entities": "Entiteiten", "mode": "Mode" - } + }, + "description": "Kies de entiteiten die u wilt opnemen. In de accessoiremodus is slechts een enkele entiteit inbegrepen. In de bridge-include-modus worden alle entiteiten in het domein opgenomen, tenzij specifieke entiteiten zijn geselecteerd. In de bridge-uitsluitingsmodus worden alle entiteiten in het domein opgenomen, behalve de uitgesloten entiteiten. Voor de beste prestaties is elke tv-mediaspeler en camera een apart HomeKit-accessoire.", + "title": "Selecteer de entiteiten die u wilt opnemen" }, "init": { "data": { - "include_domains": "Op te nemen domeinen", + "include_domains": "Domeinen om op te nemen", "mode": "modus" }, "description": "HomeKit kan worden geconfigureerd om een brug of een enkel accessoire te tonen. In de accessoiremodus kan slechts \u00e9\u00e9n entiteit worden gebruikt. De accessoiremodus is vereist om mediaspelers met de tv-apparaatklasse correct te laten werken. Entiteiten in de \"Op te nemen domeinen\" zullen worden blootgesteld aan HomeKit. U kunt op het volgende scherm selecteren welke entiteiten u wilt opnemen of uitsluiten van deze lijst.", diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 4748fe63af2..6e13907d057 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 vil et eget HomeKit-tilbeh\u00f8r v\u00e6re 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 og kamera.", "title": "Velg enheter som skal inkluderes" }, "init": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index d00744e4cb4..ffc0ac34eae 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 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", + "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.", "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/type_covers.py b/homeassistant/components/homekit/type_covers.py index ca375bb6f37..f21287b3bf8 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -364,18 +364,17 @@ class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory): """Move cover to value if call came from HomeKit.""" _LOGGER.debug("%s: Set position to %d", self.entity_id, value) - if self._supports_stop: - if value > 70: - service, position = (SERVICE_OPEN_COVER, 100) - elif value < 30: - service, position = (SERVICE_CLOSE_COVER, 0) - else: - service, position = (SERVICE_STOP_COVER, 50) + if ( + self._supports_stop + and value > 70 + or not self._supports_stop + and value >= 50 + ): + service, position = (SERVICE_OPEN_COVER, 100) + elif value < 30 or not self._supports_stop: + service, position = (SERVICE_CLOSE_COVER, 0) else: - if value >= 50: - service, position = (SERVICE_OPEN_COVER, 100) - else: - service, position = (SERVICE_CLOSE_COVER, 0) + service, position = (SERVICE_STOP_COVER, 50) params = {ATTR_ENTITY_ID: self.entity_id} self.async_call_service(DOMAIN, service, params) diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 6e1978d9499..a4a73abf998 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -240,6 +240,8 @@ class HumidifierDehumidifier(HomeAccessory): # Update target humidity target_humidity = new_state.attributes.get(ATTR_HUMIDITY) - if isinstance(target_humidity, (int, float)): - if self.char_target_humidity.value != target_humidity: - self.char_target_humidity.set_value(target_humidity) + if ( + isinstance(target_humidity, (int, float)) + and self.char_target_humidity.value != target_humidity + ): + self.char_target_humidity.set_value(target_humidity) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 140940dda47..17e2eee46e8 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -78,9 +78,11 @@ class Lock(HomeAccessory): # LockTargetState only supports locked and unlocked # Must set lock target state before current state # or there will be no notification - if hass_state in (STATE_LOCKED, STATE_UNLOCKED): - if self.char_target_state.value != current_lock_state: - self.char_target_state.set_value(current_lock_state) + if ( + hass_state in (STATE_LOCKED, STATE_UNLOCKED) + and self.char_target_state.value != current_lock_state + ): + self.char_target_state.set_value(current_lock_state) # Set lock current state ONLY after ensuring that # target state is correct or there will be no diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index b54b62372f9..5cd27109bd8 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -1,7 +1,7 @@ """Class to hold all media player accessories.""" import logging -from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION +from pyhap.const import CATEGORY_SWITCH from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, @@ -42,17 +42,9 @@ from .accessories import TYPES, HomeAccessory from .const import ( ATTR_KEY_NAME, CHAR_ACTIVE, - CHAR_ACTIVE_IDENTIFIER, - CHAR_CONFIGURED_NAME, - CHAR_CURRENT_VISIBILITY_STATE, - CHAR_IDENTIFIER, - CHAR_INPUT_SOURCE_TYPE, - CHAR_IS_CONFIGURED, CHAR_MUTE, CHAR_NAME, CHAR_ON, - CHAR_REMOTE_KEY, - CHAR_SLEEP_DISCOVER_MODE, CHAR_VOLUME, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, @@ -62,43 +54,15 @@ from .const import ( FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, - KEY_ARROW_DOWN, - KEY_ARROW_LEFT, - KEY_ARROW_RIGHT, - KEY_ARROW_UP, - KEY_BACK, - KEY_EXIT, - KEY_FAST_FORWARD, - KEY_INFORMATION, - KEY_NEXT_TRACK, KEY_PLAY_PAUSE, - KEY_PREVIOUS_TRACK, - KEY_REWIND, - KEY_SELECT, - SERV_INPUT_SOURCE, SERV_SWITCH, - SERV_TELEVISION, SERV_TELEVISION_SPEAKER, ) +from .type_remotes import REMOTE_KEYS, RemoteInputSelectAccessory from .util import get_media_player_features _LOGGER = logging.getLogger(__name__) -MEDIA_PLAYER_KEYS = { - 0: KEY_REWIND, - 1: KEY_FAST_FORWARD, - 2: KEY_NEXT_TRACK, - 3: KEY_PREVIOUS_TRACK, - 4: KEY_ARROW_UP, - 5: KEY_ARROW_DOWN, - 6: KEY_ARROW_LEFT, - 7: KEY_ARROW_RIGHT, - 8: KEY_SELECT, - 9: KEY_BACK, - 10: KEY_EXIT, - 11: KEY_PLAY_PAUSE, - 15: KEY_INFORMATION, -} # Names may not contain special characters # or emjoi (/ is a special character for Apple) @@ -250,22 +214,22 @@ class MediaPlayer(HomeAccessory): @TYPES.register("TelevisionMediaPlayer") -class TelevisionMediaPlayer(HomeAccessory): +class TelevisionMediaPlayer(RemoteInputSelectAccessory): """Generate a Television Media Player accessory.""" def __init__(self, *args): - """Initialize a Switch accessory object.""" - super().__init__(*args, category=CATEGORY_TELEVISION) + """Initialize a Television Media Player accessory object.""" + super().__init__( + SUPPORT_SELECT_SOURCE, + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + *args, + ) state = self.hass.states.get(self.entity_id) - - self.support_select_source = False - - self.sources = [] - - self.chars_tv = [CHAR_REMOTE_KEY] - self.chars_speaker = [] features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + self.chars_speaker = [] + self._supports_play_pause = features & (SUPPORT_PLAY | SUPPORT_PAUSE) if features & SUPPORT_VOLUME_MUTE or features & SUPPORT_VOLUME_STEP: self.chars_speaker.extend( @@ -274,27 +238,11 @@ class TelevisionMediaPlayer(HomeAccessory): if features & SUPPORT_VOLUME_SET: self.chars_speaker.append(CHAR_VOLUME) - source_list = state.attributes.get(ATTR_INPUT_SOURCE_LIST, []) - if source_list and features & SUPPORT_SELECT_SOURCE: - self.support_select_source = True - - serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv) - self.set_primary_service(serv_tv) - serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name) - serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True) - self.char_active = serv_tv.configure_char( - CHAR_ACTIVE, setter_callback=self.set_on_off - ) - - self.char_remote_key = serv_tv.configure_char( - CHAR_REMOTE_KEY, setter_callback=self.set_remote_key - ) - if CHAR_VOLUME_SELECTOR in self.chars_speaker: serv_speaker = self.add_preload_service( SERV_TELEVISION_SPEAKER, self.chars_speaker ) - serv_tv.add_linked_service(serv_speaker) + self.serv_tv.add_linked_service(serv_speaker) name = f"{self.display_name} Volume" serv_speaker.configure_char(CHAR_NAME, value=name) @@ -318,25 +266,6 @@ class TelevisionMediaPlayer(HomeAccessory): CHAR_VOLUME, setter_callback=self.set_volume ) - if self.support_select_source: - self.sources = source_list - self.char_input_source = serv_tv.configure_char( - CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source - ) - for index, source in enumerate(self.sources): - serv_input = self.add_preload_service( - SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] - ) - serv_tv.add_linked_service(serv_input) - serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) - serv_input.configure_char(CHAR_NAME, value=source) - serv_input.configure_char(CHAR_IDENTIFIER, value=index) - serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) - input_type = 3 if "hdmi" in source.lower() else 0 - serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type) - serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) - _LOGGER.debug("%s: Added source %s", self.entity_id, source) - self.async_update_state(state) def set_on_off(self, value): @@ -377,7 +306,7 @@ class TelevisionMediaPlayer(HomeAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - key_name = MEDIA_PLAYER_KEYS.get(value) + key_name = REMOTE_KEYS.get(value) if key_name is None: _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) return @@ -393,12 +322,13 @@ class TelevisionMediaPlayer(HomeAccessory): service = SERVICE_MEDIA_PLAY_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} self.async_call_service(DOMAIN, service, params) - else: - # Unhandled keys can be handled by listening to the event bus - self.hass.bus.fire( - EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, - {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, - ) + return + + # Unhandled keys can be handled by listening to the event bus + self.hass.bus.async_fire( + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, + ) @callback def async_update_state(self, new_state): @@ -424,18 +354,4 @@ class TelevisionMediaPlayer(HomeAccessory): if self.char_mute.value != current_mute_state: self.char_mute.set_value(current_mute_state) - # Set active input - if self.support_select_source and self.sources: - source_name = new_state.attributes.get(ATTR_INPUT_SOURCE) - _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) - if source_name in self.sources: - index = self.sources.index(source_name) - if self.char_input_source.value != index: - self.char_input_source.set_value(index) - elif hk_state: - _LOGGER.warning( - "%s: Sources out of sync. Restart Home Assistant", - self.entity_id, - ) - if self.char_input_source.value != 0: - self.char_input_source.set_value(0) + self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py new file mode 100644 index 00000000000..e4f18a7c16f --- /dev/null +++ b/homeassistant/components/homekit/type_remotes.py @@ -0,0 +1,214 @@ +"""Class to hold remote accessories.""" +from abc import abstractmethod +import logging + +from pyhap.const import CATEGORY_TELEVISION + +from homeassistant.components.remote import ( + ATTR_ACTIVITY, + ATTR_ACTIVITY_LIST, + ATTR_CURRENT_ACTIVITY, + DOMAIN as REMOTE_DOMAIN, + SUPPORT_ACTIVITY, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import callback + +from .accessories import TYPES, HomeAccessory +from .const import ( + ATTR_KEY_NAME, + CHAR_ACTIVE, + CHAR_ACTIVE_IDENTIFIER, + CHAR_CONFIGURED_NAME, + CHAR_CURRENT_VISIBILITY_STATE, + CHAR_IDENTIFIER, + CHAR_INPUT_SOURCE_TYPE, + CHAR_IS_CONFIGURED, + CHAR_NAME, + CHAR_REMOTE_KEY, + CHAR_SLEEP_DISCOVER_MODE, + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + KEY_ARROW_DOWN, + KEY_ARROW_LEFT, + KEY_ARROW_RIGHT, + KEY_ARROW_UP, + KEY_BACK, + KEY_EXIT, + KEY_FAST_FORWARD, + KEY_INFORMATION, + KEY_NEXT_TRACK, + KEY_PLAY_PAUSE, + KEY_PREVIOUS_TRACK, + KEY_REWIND, + KEY_SELECT, + SERV_INPUT_SOURCE, + SERV_TELEVISION, +) + +_LOGGER = logging.getLogger(__name__) + +REMOTE_KEYS = { + 0: KEY_REWIND, + 1: KEY_FAST_FORWARD, + 2: KEY_NEXT_TRACK, + 3: KEY_PREVIOUS_TRACK, + 4: KEY_ARROW_UP, + 5: KEY_ARROW_DOWN, + 6: KEY_ARROW_LEFT, + 7: KEY_ARROW_RIGHT, + 8: KEY_SELECT, + 9: KEY_BACK, + 10: KEY_EXIT, + 11: KEY_PLAY_PAUSE, + 15: KEY_INFORMATION, +} + + +class RemoteInputSelectAccessory(HomeAccessory): + """Generate a InputSelect accessory.""" + + def __init__( + self, + required_feature, + source_key, + source_list_key, + *args, + **kwargs, + ): + """Initialize a InputSelect accessory object.""" + super().__init__(*args, category=CATEGORY_TELEVISION, **kwargs) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + self.source_key = source_key + self.sources = [] + self.support_select_source = False + if features & required_feature: + self.sources = state.attributes.get(source_list_key, []) + if self.sources: + self.support_select_source = True + + self.chars_tv = [CHAR_REMOTE_KEY] + serv_tv = self.serv_tv = self.add_preload_service( + SERV_TELEVISION, self.chars_tv + ) + self.char_remote_key = self.serv_tv.configure_char( + CHAR_REMOTE_KEY, setter_callback=self.set_remote_key + ) + self.set_primary_service(serv_tv) + serv_tv.configure_char(CHAR_CONFIGURED_NAME, value=self.display_name) + serv_tv.configure_char(CHAR_SLEEP_DISCOVER_MODE, value=True) + self.char_active = serv_tv.configure_char( + CHAR_ACTIVE, setter_callback=self.set_on_off + ) + + if not self.support_select_source: + return + + self.char_input_source = serv_tv.configure_char( + CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source + ) + for index, source in enumerate(self.sources): + serv_input = self.add_preload_service( + SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME] + ) + serv_tv.add_linked_service(serv_input) + serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source) + serv_input.configure_char(CHAR_NAME, value=source) + serv_input.configure_char(CHAR_IDENTIFIER, value=index) + serv_input.configure_char(CHAR_IS_CONFIGURED, value=True) + input_type = 3 if "hdmi" in source.lower() else 0 + serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type) + serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) + _LOGGER.debug("%s: Added source %s", self.entity_id, source) + + @abstractmethod + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + + @abstractmethod + def set_input_source(self, value): + """Send input set value if call came from HomeKit.""" + + @abstractmethod + def set_remote_key(self, value): + """Send remote key value if call came from HomeKit.""" + + @callback + def _async_update_input_state(self, hk_state, new_state): + """Update input state after state changed.""" + # Set active input + if not self.support_select_source or not self.sources: + return + source_name = new_state.attributes.get(self.source_key) + _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) + if source_name in self.sources: + index = self.sources.index(source_name) + if self.char_input_source.value != index: + self.char_input_source.set_value(index) + elif hk_state: + _LOGGER.warning( + "%s: Sources out of sync. Restart Home Assistant", + self.entity_id, + ) + if self.char_input_source.value != 0: + self.char_input_source.set_value(0) + + +@TYPES.register("ActivityRemote") +class ActivityRemote(RemoteInputSelectAccessory): + """Generate a Activity Remote accessory.""" + + def __init__(self, *args): + """Initialize a Activity Remote accessory object.""" + super().__init__( + SUPPORT_ACTIVITY, + ATTR_CURRENT_ACTIVITY, + ATTR_ACTIVITY_LIST, + *args, + ) + self.async_update_state(self.hass.states.get(self.entity_id)) + + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.async_call_service(REMOTE_DOMAIN, service, params) + + def set_input_source(self, value): + """Send input set value if call came from HomeKit.""" + _LOGGER.debug("%s: Set current input to %s", self.entity_id, value) + source = self.sources[value] + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_ACTIVITY: source} + self.async_call_service(REMOTE_DOMAIN, SERVICE_TURN_ON, params) + + def set_remote_key(self, value): + """Send remote key value if call came from HomeKit.""" + _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) + key_name = REMOTE_KEYS.get(value) + if key_name is None: + _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) + return + self.hass.bus.async_fire( + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, + ) + + @callback + def async_update_state(self, new_state): + """Update Television remote state after state changed.""" + current_state = new_state.state + # Power state remote + hk_state = 1 if current_state == STATE_ON else 0 + _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) + if self.char_active.value != hk_state: + self.char_active.set_value(hk_state) + + self._async_update_input_state(hk_state, new_state) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 28c7ea26009..b6cc4b05125 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -6,6 +6,8 @@ from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, STATE_HOME, STATE_ON, TEMP_CELSIUS, @@ -30,7 +32,6 @@ from .const import ( CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, - DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_GAS, @@ -60,6 +61,7 @@ from .util import convert_to_float, density_to_air_quality, temperature_to_homek _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_SERVICE_MAP = { + DEVICE_CLASS_CO: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int), DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int), DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 2eb63f4c840..fb3063704c2 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -253,42 +253,44 @@ class Thermostat(HomeAccessory): hvac_mode = state.state homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] - if CHAR_TARGET_HEATING_COOLING in char_values: - # Homekit will reset the mode when VIEWING the temp - # Ignore it if its the same mode - if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode: - target_hc = char_values[CHAR_TARGET_HEATING_COOLING] - if target_hc not in self.hc_homekit_to_hass: - # If the target heating cooling state we want does not - # exist on the device, we have to sort it out - # based on the the current and target temperature since - # siri will always send HC_HEAT_COOL_AUTO in this case - # and hope for the best. - hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE) - hc_current_temp = _get_current_temperature(state, self._unit) - hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT - if ( - hc_target_temp is not None - and hc_current_temp is not None - and hc_target_temp < hc_current_temp - ): - hc_fallback_order = HC_HEAT_COOL_PREFER_COOL - for hc_fallback in hc_fallback_order: - if hc_fallback in self.hc_homekit_to_hass: - _LOGGER.debug( - "Siri requested target mode: %s and the device does not support, falling back to %s", - target_hc, - hc_fallback, - ) - target_hc = hc_fallback - break + # Homekit will reset the mode when VIEWING the temp + # Ignore it if its the same mode + if ( + CHAR_TARGET_HEATING_COOLING in char_values + and char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode + ): + target_hc = char_values[CHAR_TARGET_HEATING_COOLING] + if target_hc not in self.hc_homekit_to_hass: + # If the target heating cooling state we want does not + # exist on the device, we have to sort it out + # based on the the current and target temperature since + # siri will always send HC_HEAT_COOL_AUTO in this case + # and hope for the best. + hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE) + hc_current_temp = _get_current_temperature(state, self._unit) + hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT + if ( + hc_target_temp is not None + and hc_current_temp is not None + and hc_target_temp < hc_current_temp + ): + hc_fallback_order = HC_HEAT_COOL_PREFER_COOL + for hc_fallback in hc_fallback_order: + if hc_fallback in self.hc_homekit_to_hass: + _LOGGER.debug( + "Siri requested target mode: %s and the device does not support, falling back to %s", + target_hc, + hc_fallback, + ) + target_hc = hc_fallback + break - service = SERVICE_SET_HVAC_MODE_THERMOSTAT - hass_value = self.hc_homekit_to_hass[target_hc] - params = {ATTR_HVAC_MODE: hass_value} - events.append( - f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" - ) + service = SERVICE_SET_HVAC_MODE_THERMOSTAT + hass_value = self.hc_homekit_to_hass[target_hc] + params = {ATTR_HVAC_MODE: hass_value} + events.append( + f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" + ) if CHAR_TARGET_TEMPERATURE in char_values: hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] @@ -462,23 +464,26 @@ class Thermostat(HomeAccessory): # Update current temperature current_temp = _get_current_temperature(new_state, self._unit) - if current_temp is not None: - if self.char_current_temp.value != current_temp: - self.char_current_temp.set_value(current_temp) + if current_temp is not None and self.char_current_temp.value != current_temp: + self.char_current_temp.set_value(current_temp) # Update current humidity if CHAR_CURRENT_HUMIDITY in self.chars: current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY) - if isinstance(current_humdity, (int, float)): - if self.char_current_humidity.value != current_humdity: - self.char_current_humidity.set_value(current_humdity) + if ( + isinstance(current_humdity, (int, float)) + and self.char_current_humidity.value != current_humdity + ): + self.char_current_humidity.set_value(current_humdity) # Update target humidity if CHAR_TARGET_HUMIDITY in self.chars: target_humdity = new_state.attributes.get(ATTR_HUMIDITY) - if isinstance(target_humdity, (int, float)): - if self.char_target_humidity.value != target_humdity: - self.char_target_humidity.set_value(target_humdity) + if ( + isinstance(target_humdity, (int, float)) + and self.char_target_humidity.value != target_humdity + ): + self.char_target_humidity.set_value(target_humdity) # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: @@ -575,9 +580,8 @@ class WaterHeater(HomeAccessory): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) hass_value = HC_HOMEKIT_TO_HASS[value] - if hass_value != HVAC_MODE_HEAT: - if self.char_target_heat_cool.value != 1: - self.char_target_heat_cool.set_value(1) # Heat + if hass_value != HVAC_MODE_HEAT and self.char_target_heat_cool.value != 1: + self.char_target_heat_cool.set_value(1) # Heat def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" @@ -596,14 +600,18 @@ class WaterHeater(HomeAccessory): """Update water_heater state after state change.""" # Update current and target temperature target_temperature = _get_target_temperature(new_state, self._unit) - if target_temperature is not None: - if target_temperature != self.char_target_temp.value: - self.char_target_temp.set_value(target_temperature) + if ( + target_temperature is not None + and target_temperature != self.char_target_temp.value + ): + self.char_target_temp.set_value(target_temperature) current_temperature = _get_current_temperature(new_state, self._unit) - if current_temperature is not None: - if current_temperature != self.char_current_temp.value: - self.char_current_temp.set_value(current_temperature) + if ( + current_temperature is not None + and current_temperature != self.char_current_temp.value + ): + self.char_current_temp.set_value(current_temperature) # Update display units if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 46b893bb96d..a746355e124 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -12,10 +12,12 @@ import voluptuous as vol from homeassistant.components import binary_sensor, media_player, sensor from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import ( DEVICE_CLASS_TV, DOMAIN as MEDIA_PLAYER_DOMAIN, ) +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN, SUPPORT_ACTIVITY from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -444,10 +446,11 @@ def port_is_available(port: int) -> bool: async def async_find_next_available_port(hass: HomeAssistant, start_port: int) -> int: """Find the next available port not assigned to a config entry.""" - exclude_ports = set() - for entry in hass.config_entries.async_entries(DOMAIN): - if CONF_PORT in entry.data: - exclude_ports.add(entry.data[CONF_PORT]) + exclude_ports = { + entry.data[CONF_PORT] + for entry in hass.config_entries.async_entries(DOMAIN) + if CONF_PORT in entry.data + } return await hass.async_add_executor_job( _find_next_available_port, start_port, exclude_ports @@ -487,8 +490,10 @@ def accessory_friendly_name(hass_name, accessory): see both to identify the accessory. """ accessory_mdns_name = accessory.display_name - if hass_name.startswith(accessory_mdns_name): + if hass_name.casefold().startswith(accessory_mdns_name.casefold()): return hass_name + if accessory_mdns_name.casefold().startswith(hass_name.casefold()): + return accessory_mdns_name return f"{hass_name} ({accessory_mdns_name})" @@ -497,10 +502,10 @@ def state_needs_accessory_mode(state): if state.domain == CAMERA_DOMAIN: return True - if ( - state.domain == MEDIA_PLAYER_DOMAIN + return ( + state.domain == LOCK_DOMAIN + or state.domain == MEDIA_PLAYER_DOMAIN and state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TV - ): - return True - - return False + or state.domain == REMOTE_DOMAIN + and state.attributes.get(ATTR_SUPPORTED_FEATURES) & SUPPORT_ACTIVITY + ) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 0a8f376fb33..d7b28036426 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,5 +1,7 @@ """Support for Homekit device discovery.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any import aiohomekit from aiohomekit.model import Accessory @@ -77,7 +79,7 @@ class HomeKitEntity(Entity): signal_remove() self._signals.clear() - async def async_put_characteristics(self, characteristics: Dict[str, Any]): + async def async_put_characteristics(self, characteristics: dict[str, Any]): """ Write characteristics to the device. diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 896034a2ca0..2a162eb2b2a 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -69,7 +69,7 @@ class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): return self.service.value(CharacteristicsTypes.DENSITY_VOC) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" data = {"air_quality_text": self.air_quality_text} diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 621fb01ff74..c40252c9fdc 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -105,7 +105,7 @@ class HomeKitAlarmControlPanelEntity(HomeKitEntity, AlarmControlPanelEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" battery_level = self.service.value(CharacteristicsTypes.BATTERY_LEVEL) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index cb0feb6ba77..2c251d41fb3 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -132,8 +132,8 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): else: hvac_mode = TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(state) _LOGGER.warning( - "HomeKit device %s: Setting temperature in %s mode is not supported yet." - " Consider raising a ticket if you have this device and want to help us implement this feature.", + "HomeKit device %s: Setting temperature in %s mode is not supported yet;" + " Consider raising a ticket if you have this device and want to help us implement this feature", self.entity_id, hvac_mode, ) @@ -147,8 +147,8 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): return if hvac_mode not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: _LOGGER.warning( - "HomeKit device %s: Setting temperature in %s mode is not supported yet." - " Consider raising a ticket if you have this device and want to help us implement this feature.", + "HomeKit device %s: Setting temperature in %s mode is not supported yet;" + " Consider raising a ticket if you have this device and want to help us implement this feature", self.entity_id, hvac_mode, ) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 38a41617c6a..fcf83918fda 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -18,8 +18,6 @@ from .const import DOMAIN, KNOWN_DEVICES HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" -HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" -HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" HOMEKIT_IGNORE = [ # eufy Indoor Cam 2K and 2K Pan & Tilt @@ -181,8 +179,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="no_devices") - async def _hkid_is_homekit_bridge(self, hkid): - """Determine if the device is a homekit bridge.""" + async def _hkid_is_homekit(self, hkid): + """Determine if the device is a homekit bridge or accessory.""" dev_reg = await async_get_device_registry(self.hass) device = dev_reg.async_get_device( identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)} @@ -190,7 +188,13 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if device is None: return False - return device.model == HOMEKIT_BRIDGE_MODEL + + for entry_id in device.config_entries: + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry and entry.domain == HOMEKIT_BRIDGE_DOMAIN: + return True + + return False async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. @@ -266,8 +270,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if model in HOMEKIT_IGNORE: return self.async_abort(reason="ignored_model") - # If this is a HomeKit bridge exported by *this* HA instance ignore it. - if await self._hkid_is_homekit_bridge(hkid): + # If this is a HomeKit bridge/accessory exported by *this* HA instance ignore it. + if await self._hkid_is_homekit(hkid): return self.async_abort(reason="ignored_model") self.name = name diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 6c945c81115..dd25e32b3c4 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -108,7 +108,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" obstruction_detected = self.service.value( CharacteristicsTypes.OBSTRUCTION_DETECTED @@ -235,7 +235,7 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" obstruction_detected = self.service.value( CharacteristicsTypes.OBSTRUCTION_DETECTED diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index b2e668915d7..c9cd771edf6 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for homekit devices.""" -from typing import List +from __future__ import annotations from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import InputEventValues @@ -75,11 +75,14 @@ class TriggerSource: automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None def event_handler(char): if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: return - self._hass.async_create_task(action({"trigger": config})) + self._hass.async_create_task( + action({"trigger": {**config, "id": trigger_id}}) + ) trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] @@ -99,9 +102,11 @@ def enumerate_stateless_switch(service): # A stateless switch that has a SERVICE_LABEL_INDEX is part of a group # And is handled separately - if service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX): - if len(service.linked) > 0: - return [] + if ( + service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX) + and len(service.linked) > 0 + ): + return [] char = service[CharacteristicsTypes.INPUT_EVENT] @@ -109,17 +114,15 @@ def enumerate_stateless_switch(service): # manufacturer might not - clamp options to what they say. all_values = clamp_enum_to_char(InputEventValues, char) - results = [] - for event_type in all_values: - results.append( - { - "characteristic": char.iid, - "value": event_type, - "type": "button1", - "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], - } - ) - return results + return [ + { + "characteristic": char.iid, + "value": event_type, + "type": "button1", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + for event_type in all_values + ] def enumerate_stateless_switch_group(service): @@ -226,7 +229,7 @@ def async_fire_triggers(conn, events): source.fire(iid, ev) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for homekit devices.""" if device_id not in hass.data.get(TRIGGERS, {}): @@ -234,20 +237,16 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: device = hass.data[TRIGGERS][device_id] - triggers = [] - - for trigger, subtype in device.async_get_triggers(): - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: trigger, - CONF_SUBTYPE: subtype, - } - ) - - return triggers + return [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + for trigger, subtype in device.async_get_triggers() + ] async def async_attach_trigger( @@ -257,8 +256,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - device_id = config[CONF_DEVICE_ID] device = hass.data[TRIGGERS][device_id] return await device.async_attach_trigger(config, action, automation_info) diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index e4bed25d618..227174d00e9 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -1,5 +1,5 @@ """Support for HomeKit Controller humidifier.""" -from typing import List, Optional +from __future__ import annotations from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -69,14 +69,14 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) @property - def target_humidity(self) -> Optional[int]: + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return self.service.value( CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD ) @property - def mode(self) -> Optional[str]: + def mode(self) -> str | None: """Return the current mode, e.g., home, auto, baby. Requires SUPPORT_MODES. @@ -87,7 +87,7 @@ class HomeKitHumidifier(HomeKitEntity, HumidifierEntity): return MODE_AUTO if mode == 1 else MODE_NORMAL @property - def available_modes(self) -> Optional[List[str]]: + def available_modes(self) -> list[str] | None: """Return a list of available modes. Requires SUPPORT_MODES. @@ -175,14 +175,14 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) @property - def target_humidity(self) -> Optional[int]: + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return self.service.value( CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD ) @property - def mode(self) -> Optional[str]: + def mode(self) -> str | None: """Return the current mode, e.g., home, auto, baby. Requires SUPPORT_MODES. @@ -193,7 +193,7 @@ class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity): return MODE_AUTO if mode == 1 else MODE_NORMAL @property - def available_modes(self) -> Optional[List[str]]: + def available_modes(self) -> list[str] | None: """Return a list of available modes. Requires SUPPORT_MODES. diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 8ac7fd608fd..09c02ce0ff9 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -63,7 +63,7 @@ class HomeKitLock(HomeKitEntity, LockEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" attributes = {} diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 6dfa8720ee5..71bde5f0af9 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -93,9 +93,11 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): if TargetMediaStateValues.STOP in self.supported_media_states: features |= SUPPORT_STOP - if self.service.has(CharacteristicsTypes.REMOTE_KEY): - if RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys: - features |= SUPPORT_PAUSE | SUPPORT_PLAY + if ( + self.service.has(CharacteristicsTypes.REMOTE_KEY) + and RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys + ): + features |= SUPPORT_PAUSE | SUPPORT_PLAY return features diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 094d0a500d1..2ae264fabb9 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -2,6 +2,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, @@ -39,7 +40,7 @@ SIMPLE_SENSOR = { } -class HomeKitHumiditySensor(HomeKitEntity): +class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit humidity sensor.""" def get_characteristic_types(self): @@ -72,7 +73,7 @@ class HomeKitHumiditySensor(HomeKitEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) -class HomeKitTemperatureSensor(HomeKitEntity): +class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit temperature sensor.""" def get_characteristic_types(self): @@ -105,7 +106,7 @@ class HomeKitTemperatureSensor(HomeKitEntity): return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) -class HomeKitLightSensor(HomeKitEntity): +class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit light level sensor.""" def get_characteristic_types(self): @@ -138,7 +139,7 @@ class HomeKitLightSensor(HomeKitEntity): return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) -class HomeKitCarbonDioxideSensor(HomeKitEntity): +class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): """Representation of a Homekit Carbon Dioxide sensor.""" def get_characteristic_types(self): @@ -166,7 +167,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity): return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) -class HomeKitBatterySensor(HomeKitEntity): +class HomeKitBatterySensor(HomeKitEntity, SensorEntity): """Representation of a Homekit battery sensor.""" def get_characteristic_types(self): @@ -233,7 +234,7 @@ class HomeKitBatterySensor(HomeKitEntity): return self.service.value(CharacteristicsTypes.BATTERY_LEVEL) -class SimpleSensor(CharacteristicEntity): +class SimpleSensor(CharacteristicEntity, SensorEntity): """ A simple sensor for a single characteristic. diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index b9d0b273cb1..36ed379bc80 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -39,7 +39,7 @@ class HomeKitSwitch(HomeKitEntity, SwitchEntity): await self.async_put_characteristics({CharacteristicsTypes.ON: False}) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE) if outlet_in_use is not None: @@ -77,7 +77,7 @@ class HomeKitValve(HomeKitEntity, SwitchEntity): return self.service.value(CharacteristicsTypes.ACTIVE) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" attrs = {} diff --git a/homeassistant/components/homekit_controller/translations/bg.json b/homeassistant/components/homekit_controller/translations/bg.json index 8e2762f9f32..01439889734 100644 --- a/homeassistant/components/homekit_controller/translations/bg.json +++ b/homeassistant/components/homekit_controller/translations/bg.json @@ -34,5 +34,19 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "\u0411\u0443\u0442\u043e\u043d 1", + "button10": "\u0411\u0443\u0442\u043e\u043d 10", + "button2": "\u0411\u0443\u0442\u043e\u043d 2", + "button3": "\u0411\u0443\u0442\u043e\u043d 3", + "button4": "\u0411\u0443\u0442\u043e\u043d 4", + "button5": "\u0411\u0443\u0442\u043e\u043d 5", + "button6": "\u0411\u0443\u0442\u043e\u043d 6", + "button7": "\u0411\u0443\u0442\u043e\u043d 7", + "button8": "\u0411\u0443\u0442\u043e\u043d 8", + "button9": "\u0411\u0443\u0442\u043e\u043d 9" + } + }, "title": "HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 49e6fc53231..90e2405ed64 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.", "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.", - "already_in_progress": "Az eszk\u00f6z konfigur\u00e1ci\u00f3ja m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", @@ -30,9 +30,29 @@ "device": "Eszk\u00f6z" }, "description": "V\u00e1lassza ki azt az eszk\u00f6zt, amelyet p\u00e1ros\u00edtani szeretne", - "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" + "title": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Gomb 1", + "button10": "Gomb 10", + "button2": "Gomb 2", + "button3": "Gomb 3", + "button4": "Gomb 4", + "button5": "Gomb 5", + "button6": "Gomb 6", + "button7": "Gomb 7", + "button8": "Gomb 8", + "button9": "Gomb 9", + "doorbell": "Cseng\u0151" + }, + "trigger_type": { + "double_press": "\"{subtype}\" k\u00e9tszer lenyomva", + "long_press": "\"{subtype}\" lenyomva \u00e9s nyomva tartva", + "single_press": "\"{subtype}\" lenyomva" + } + }, "title": "HomeKit Vez\u00e9rl\u0151" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json new file mode 100644 index 00000000000..49a37d3b3fb --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -0,0 +1,71 @@ +{ + "config": { + "abort": { + "accessory_not_found_error": "Tidak dapat menambahkan pemasangan karena perangkat tidak dapat ditemukan lagi.", + "already_configured": "Aksesori sudah dikonfigurasi dengan pengontrol ini.", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "already_paired": "Aksesori ini sudah dipasangkan ke perangkat lain. Setel ulang aksesori dan coba lagi.", + "ignored_model": "Dukungan HomeKit untuk model ini diblokir karena integrasi asli dengan fitur lebih lengkap telah tersedia.", + "invalid_config_entry": "Perangkat ini ditampilkan sebagai siap untuk dipasangkan tetapi sudah ada entri konfigurasi yang bertentangan untuk perangkat tersebut dalam Home Assistant, yang harus dihapus terlebih dulu.", + "invalid_properties": "Properti tidak valid diumumkan oleh perangkat.", + "no_devices": "Tidak ada perangkat yang belum dipasangkan yang dapat ditemukan" + }, + "error": { + "authentication_error": "Kode HomeKit salah. Periksa dan coba lagi.", + "max_peers_error": "Perangkat menolak untuk menambahkan pemasangan karena tidak memiliki penyimpanan pemasangan yang tersedia.", + "pairing_failed": "Terjadi kesalahan yang tidak tertangani saat mencoba memasangkan dengan perangkat ini. Ini mungkin kegagalan sementara atau perangkat Anda mungkin tidak didukung saat ini.", + "unable_to_pair": "Gagal memasangkan, coba lagi.", + "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal." + }, + "flow_title": "{name} lewat HomeKit Accessory Protocol", + "step": { + "busy_error": { + "description": "Batalkan pemasangan di semua pengontrol, atau coba mulai ulang perangkat, lalu lanjutkan untuk melanjutkan pemasangan.", + "title": "Perangkat sudah dipasangkan dengan pengontrol lain" + }, + "max_tries_error": { + "description": "Perangkat telah menerima lebih dari 100 upaya autentikasi yang gagal. Coba mulai ulang perangkat, lalu lanjutkan pemasangan.", + "title": "Upaya autentikasi maksimum terlampaui" + }, + "pair": { + "data": { + "pairing_code": "Kode Pemasangan" + }, + "description": "Pengontrol HomeKit berkomunikasi dengan {name} melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", + "title": "Pasangkan dengan perangkat melalui HomeKit Accessory Protocol" + }, + "protocol_error": { + "description": "Perangkat mungkin tidak dalam mode pemasangan dan mungkin memerlukan tombol fisik atau virtual. Pastikan perangkat dalam mode pemasangan atau coba mulai ulang perangkat, lalu lanjutkan pemasangan.", + "title": "Terjadi kesalahan saat berkomunikasi dengan aksesori" + }, + "user": { + "data": { + "device": "Perangkat" + }, + "description": "Pengontrol HomeKit berkomunikasi melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Pilih perangkat yang ingin Anda pasangkan:", + "title": "Pemilihan perangkat" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button1": "Tombol 1", + "button10": "Tombol 10", + "button2": "Tombol 2", + "button3": "Tombol 3", + "button4": "Tombol 4", + "button5": "Tombol 5", + "button6": "Tombol 6", + "button7": "Tombol 7", + "button8": "Tombol 8", + "button9": "Tombol 9", + "doorbell": "Bel pintu" + }, + "trigger_type": { + "double_press": "\"{subtype}\" ditekan dua kali", + "long_press": "\"{subtype}\" ditekan dan ditahan", + "single_press": "\"{subtype}\" ditekan" + } + }, + "title": "Pengontrol HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json index 7314f43545e..c28573ee6ac 100644 --- a/homeassistant/components/homekit_controller/translations/ko.json +++ b/homeassistant/components/homekit_controller/translations/ko.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc73c\ubbc0\ub85c \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "already_configured": "\uc561\uc138\uc11c\ub9ac\uac00 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.", - "invalid_properties": "\uc7a5\uce58\uc5d0\uc11c\uc120\uc5b8\ud55c \uc798\ubabb\ub41c \uc18d\uc131\uc785\ub2c8\ub2e4.", + "invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.", + "invalid_properties": "\uae30\uae30\uc5d0\uc11c \uc798\ubabb\ub41c \uc18d\uc131\uc744 \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4.", "no_devices": "\ud398\uc5b4\ub9c1\uc774 \ud544\uc694\ud55c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "error": { "authentication_error": "HomeKit \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud655\uc778 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "max_peers_error": "\uae30\uae30\uc5d0 \ube44\uc5b4\uc788\ub294 \ud398\uc5b4\ub9c1 \uc7a5\uc18c\uac00 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1 \ucd94\uac00\ub97c \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "max_peers_error": "\uae30\uae30\uc5d0 \ube44\uc5b4\uc788\ub294 \ud398\uc5b4\ub9c1 \uc7a5\uc18c\uac00 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "pairing_failed": "\uc774 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\uc744 \uc2dc\ub3c4\ud558\ub294 \uc911 \ucc98\ub9ac\ub418\uc9c0 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc77c\uc2dc\uc801\uc778 \uc624\ub958\uc774\uac70\ub098 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uae30\uae30 \uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "unable_to_pair": "\ud398\uc5b4\ub9c1 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "unknown_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218\uc5c6\ub294 \uc624\ub958\ub97c \ubcf4\uace0\ud588\uc2b5\ub2c8\ub2e4. \ud398\uc5b4\ub9c1\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4." @@ -21,7 +21,7 @@ "step": { "busy_error": { "description": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864\ub7ec\uc5d0\uc11c \ud398\uc5b4\ub9c1\uc744 \uc911\ub2e8\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", - "title": "\uae30\uae30\uac00 \uc774\ubbf8 \ub2e4\ub978 \ucee8\ud2b8\ub864\ub7ec\uc640 \ud398\uc774\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4" + "title": "\uae30\uae30\uac00 \uc774\ubbf8 \ub2e4\ub978 \ucee8\ud2b8\ub864\ub7ec\uc640 \ud398\uc774\ub9c1 \ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4" }, "max_tries_error": { "description": "\uae30\uae30\uac00 100\ud68c \uc774\uc0c1\uc758 \uc2e4\ud328\ud55c \uc778\uc99d \uc2dc\ub3c4\ub97c \ubc1b\uc558\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", @@ -31,11 +31,11 @@ "data": { "pairing_code": "\ud398\uc5b4\ub9c1 \ucf54\ub4dc" }, - "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c {name} \uacfc(\uc640) \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc (XX-XX-XXX \ud615\uc2dd) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774 \ucf54\ub4dc\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \uae30\uae30\ub098 \ud3ec\uc7a5 \ubc15\uc2a4\uc5d0 \ud45c\uc2dc\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud504\ub85c\ud1a0\ucf5c\uc744 \ud1b5\ud574 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1 \ud558\uae30" + "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c {name}\uacfc(\uc640) \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc(XX-XX-XXX \ud615\uc2dd)\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774 \ucf54\ub4dc\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \uae30\uae30\ub098 \ud3ec\uc7a5 \ubc15\uc2a4\uc5d0 \ud45c\uc2dc\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud504\ub85c\ud1a0\ucf5c\ub85c \uae30\uae30\uc640 \ud398\uc5b4\ub9c1 \ud558\uae30" }, "protocol_error": { - "description": "\uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\uba70 \ubb3c\ub9ac\uc801 \ub610\ub294 \uac00\uc0c1 \uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", + "description": "\uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\uba70 \ubb3c\ub9ac\uc801 \ub610\ub294 \uac00\uc0c1\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", "title": "\uc561\uc138\uc11c\ub9ac\uc640 \ud1b5\uc2e0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "user": { @@ -62,9 +62,9 @@ "doorbell": "\ucd08\uc778\uc885" }, "trigger_type": { - "double_press": "\" {subtype} \"\uc744 \ub450\ubc88 \ub204\ub984", - "long_press": "\" {subtype} \"\uc744 \uae38\uac8c \ub204\ub984", - "single_press": "\"{subtype}\" \uc744 \ud55c\ubc88 \ub204\ub984" + "double_press": "\"{subtype}\"\uc774(\uac00) \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c", + "long_press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub824\uc9c4 \ucc44\ub85c \uc788\uc744 \ub54c", + "single_press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub838\uc744 \ub54c" } }, "title": "HomeKit \ucee8\ud2b8\ub864\ub7ec" diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index ce4279229ab..57692426ce0 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Kan geen koppeling toevoegen omdat het apparaat niet langer kan worden gevonden.", "already_configured": "Accessoire is al geconfigureerd met deze controller.", - "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.", + "already_in_progress": "De configuratiestroom is al aan de gang", "already_paired": "Dit accessoire is al gekoppeld aan een ander apparaat. Reset het accessoire en probeer het opnieuw.", "ignored_model": "HomeKit-ondersteuning voor dit model is geblokkeerd omdat er een meer functie volledige native integratie beschikbaar is.", "invalid_config_entry": "Dit apparaat geeft aan dat het gereed is om te koppelen, maar er is al een conflicterend configuratie-item voor in de Home Assistant dat eerst moet worden verwijderd.", @@ -17,21 +17,33 @@ "unable_to_pair": "Kan niet koppelen, probeer het opnieuw.", "unknown_error": "Apparaat meldde een onbekende fout. Koppelen mislukt." }, - "flow_title": "HomeKit-accessoire: {name}", + "flow_title": "{name} via HomeKit-accessoireprotocol", "step": { + "busy_error": { + "description": "Onderbreek het koppelen op alle controllers, of probeer het apparaat opnieuw op te starten, en ga dan verder om het koppelen te hervatten.", + "title": "Het apparaat is al aan het koppelen met een andere controller" + }, + "max_tries_error": { + "description": "Het apparaat heeft meer dan 100 mislukte verificatiepogingen ontvangen. Probeer het apparaat opnieuw op te starten en ga dan verder om het koppelen te hervatten.", + "title": "Maximum aantal authenticatiepogingen overschreden" + }, "pair": { "data": { "pairing_code": "Koppelingscode" }, - "description": "Voer uw HomeKit pairing code (in het formaat XXX-XX-XXX) om dit accessoire te gebruiken", + "description": "HomeKit Controller communiceert met {name} via het lokale netwerk met behulp van een beveiligde versleutelde verbinding zonder een aparte HomeKit-controller of iCloud. Voer uw HomeKit-koppelcode in (in de indeling XXX-XX-XXX) om dit accessoire te gebruiken. Deze code is meestal te vinden op het apparaat zelf of in de verpakking.", "title": "Koppel met HomeKit accessoire" }, + "protocol_error": { + "description": "Het apparaat staat mogelijk niet in de koppelingsmodus en vereist mogelijk een fysieke of virtuele druk op de knop. Zorg ervoor dat het apparaat in de koppelingsmodus staat of probeer het apparaat opnieuw op te starten en ga dan verder om het koppelen te hervatten.", + "title": "Fout bij het communiceren met de accessoire" + }, "user": { "data": { "device": "Apparaat" }, - "description": "Selecteer het apparaat waarmee u wilt koppelen", - "title": "Koppel met HomeKit accessoire" + "description": "HomeKit Controller communiceert via het lokale netwerk met behulp van een veilige versleutelde verbinding zonder een aparte HomeKit-controller of iCloud. Selecteer het apparaat dat u wilt koppelen:", + "title": "Apparaat selectie" } } }, @@ -55,5 +67,5 @@ "single_press": "\" {subtype} \" ingedrukt" } }, - "title": "HomeKit Accessoires" + "title": "HomeKit Controller" } \ No newline at end of file diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 3b77b90ff25..aa5fb4a8e44 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -7,6 +7,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -92,14 +93,14 @@ class HMThermostat(HMDevice, ClimateEntity): return "boost" if not self._hm_control_mode: - return None + return PRESET_NONE mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() # Filter HVAC states if mode not in (HVAC_MODE_AUTO, HVAC_MODE_HEAT): - return None + return PRESET_NONE return mode @property diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index e8fa272b0e5..864441c2aa6 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -60,6 +60,7 @@ HM_DEVICE_TYPES = { "IPWSwitch", "IOSwitchWireless", "IPWIODevice", + "IPSwitchBattery", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -119,6 +120,8 @@ HM_DEVICE_TYPES = { "ValveBox", "IPKeyBlind", "IPKeyBlindTilt", + "IPLanRouter", + "TempModuleSTE2", ], DISCOVER_CLIMATE: [ "Thermostat", @@ -163,6 +166,7 @@ HM_DEVICE_TYPES = { "IPWMotionDection", "IPAlarmSensor", "IPRainSensor", + "IPLanRouter", ], DISCOVER_COVER: [ "Blind", diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index bb87d691fc0..50b9bcb2bfc 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -71,7 +71,7 @@ class HMDevice(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" # Static attributes attr = { @@ -231,7 +231,7 @@ class HMHub(Entity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._variables.copy() diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 11731e2ae5f..036034bf801 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -9,6 +9,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_TRANSITION, LightEntity, ) @@ -53,7 +54,8 @@ class HMLight(HMDevice, LightEntity): @property def supported_features(self): """Flag supported features.""" - features = SUPPORT_BRIGHTNESS + features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + if "COLOR" in self._hmdevice.WRITENODE: features |= SUPPORT_COLOR if "PROGRAM" in self._hmdevice.WRITENODE: @@ -95,7 +97,7 @@ class HMLight(HMDevice, LightEntity): def turn_on(self, **kwargs): """Turn the light on and/or change color or color effect settings.""" if ATTR_TRANSITION in kwargs: - self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION]) + self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel) if ATTR_BRIGHTNESS in kwargs and self._state == "LEVEL": percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 @@ -123,6 +125,9 @@ class HMLight(HMDevice, LightEntity): def turn_off(self, **kwargs): """Turn the light off.""" + if ATTR_TRANSITION in kwargs: + self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel) + self._hmdevice.off(self._channel) def _init_data_struct(self): diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 36414b606f9..d81dc97cdb7 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,6 +2,6 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.71"], + "requirements": ["pyhomematic==0.1.72"], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index e6439c451c1..4525d5a48fc 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,6 +1,7 @@ """Support for HomeMatic sensors.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, @@ -97,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HMSensor(HMDevice): +class HMSensor(HMDevice, SensorEntity): """Representation of a HomeMatic sensor.""" @property diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 51cec6ac0cd..7fa5e197aa8 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for HomematicIP Cloud alarm control panel.""" +from __future__ import annotations + import logging -from typing import Any, Dict +from typing import Any from homematicip.functionalHomes import SecurityAndAlarmHome @@ -44,7 +46,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): _LOGGER.info("Setting up %s", self.name) @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4f1ae523ecc..4fcf1f67dd4 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,5 +1,7 @@ """Support for HomematicIP Cloud binary sensor.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any from homematicip.aio.device import ( AsyncAccelerationSensor, @@ -166,7 +168,7 @@ class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEnt return name if not self._home.name else f"{self._home.name} {name}" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device specific attributes.""" # Adds a sensor to the existing HAP device return { @@ -210,9 +212,9 @@ class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity): return self._device.accelerationSensorTriggered @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the acceleration sensor.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -285,9 +287,9 @@ class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEn return DEVICE_CLASS_DOOR @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the Shutter Contact.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes if self.has_additional_state: window_state = getattr(self._device, "windowState", None) @@ -412,9 +414,9 @@ class HomematicipSunshineSensor(HomematicipGenericEntity, BinarySensorEntity): return self._device.sunshine @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the illuminance sensor.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes today_sunshine_duration = getattr(self._device, "todaySunshineDuration", None) if today_sunshine_duration: @@ -482,9 +484,9 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorE return True @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the security zone group.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -526,9 +528,9 @@ class HomematicipSecuritySensorGroup( super().__init__(hap, device, post="Sensors") @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the security group.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes smoke_detector_at = getattr(self._device, "smokeDetectorAlarmType", None) if smoke_detector_at: diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index dcd5aeb284e..5cdadf4d5f1 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,5 +1,7 @@ """Support for HomematicIP Cloud climate devices.""" -from typing import Any, Dict, List, Optional, Union +from __future__ import annotations + +from typing import Any from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup @@ -71,7 +73,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): self._simple_heating = self._first_radiator_thermostat @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, self._device.id)}, @@ -121,7 +123,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return HVAC_MODE_AUTO @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" if self._disabled_by_cooling_mode and not self._has_switch: return [HVAC_MODE_OFF] @@ -133,7 +135,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): ) @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """ Return the current hvac_action. @@ -151,7 +153,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return None @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode.""" if self._device.boostMode: return PRESET_BOOST @@ -174,7 +176,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): ) @property - def preset_modes(self) -> List[str]: + def preset_modes(self) -> list[str]: """Return a list of available preset modes incl. hmip profiles.""" # Boost is only available if a radiator thermostat is in the room, # and heat mode is enabled. @@ -237,9 +239,9 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): await self._device.set_active_profile(profile_idx) @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the access point.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes if self._device.controlMode == HMIP_ECO_CM: if self._indoor_climate.absenceType in [ @@ -259,7 +261,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): return self._home.get_functionalHome(IndoorClimateHome) @property - def _device_profiles(self) -> List[str]: + def _device_profiles(self) -> list[str]: """Return the relevant profiles.""" return [ profile @@ -270,7 +272,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): ] @property - def _device_profile_names(self) -> List[str]: + def _device_profile_names(self) -> list[str]: """Return a collection of profile names.""" return [profile.name for profile in self._device_profiles] @@ -298,7 +300,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): ) @property - def _relevant_profile_group(self) -> List[str]: + def _relevant_profile_group(self) -> list[str]: """Return the relevant profile groups.""" if self._disabled_by_cooling_mode: return [] @@ -322,7 +324,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @property def _first_radiator_thermostat( self, - ) -> Optional[Union[AsyncHeatingThermostat, AsyncHeatingThermostatCompact]]: + ) -> AsyncHeatingThermostat | AsyncHeatingThermostatCompact | None: """Return the first radiator thermostat from the hmip heating group.""" for device in self._device.devices: if isinstance( diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index b6b78948894..d90d8d7023b 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure the HomematicIP Cloud component.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -27,11 +29,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) -> dict[str, Any]: """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) -> dict[str, Any]: """Handle a flow start.""" errors = {} @@ -62,7 +64,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) -> dict[str, Any]: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -84,7 +86,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) -> dict[str, Any]: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID].replace("-", "").upper() authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 4fb21febb40..31ccb8b9bc7 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(".") DOMAIN = "homematicip_cloud" -COMPONENTS = [ +PLATFORMS = [ ALARM_CONTROL_PANEL_DOMAIN, BINARY_SENSOR_DOMAIN, CLIMATE_DOMAIN, diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 29a06c558fe..aa1be11758e 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,5 +1,5 @@ """Support for HomematicIP Cloud cover devices.""" -from typing import Optional +from __future__ import annotations from homematicip.aio.device import ( AsyncBlindModule, @@ -95,7 +95,7 @@ class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): ) @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._device.primaryShadingLevel is not None: return self._device.primaryShadingLevel == HMIP_COVER_CLOSED @@ -168,7 +168,7 @@ class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): await self._device.set_shutter_level(level, self._channel) @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._device.functionalChannels[self._channel].shutterLevel is not None: return ( @@ -265,7 +265,7 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): return door_state_to_position.get(self._device.doorState) @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return if the cover is closed.""" return self._device.doorState == DoorState.CLOSED @@ -305,7 +305,7 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): return None @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._device.shutterLevel is not None: return self._device.shutterLevel == HMIP_COVER_CLOSED diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index a1e13658d20..856e47a1dee 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -1,6 +1,8 @@ """Generic entity for the HomematicIP Cloud component.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup @@ -74,9 +76,9 @@ class HomematicipGenericEntity(Entity): self, hap: HomematicipHAP, device, - post: Optional[str] = None, - channel: Optional[int] = None, - is_multi_channel: Optional[bool] = False, + post: str | None = None, + channel: int | None = None, + is_multi_channel: bool | None = False, ) -> None: """Initialize the generic entity.""" self._hap = hap @@ -90,7 +92,7 @@ class HomematicipGenericEntity(Entity): _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): @@ -223,7 +225,7 @@ class HomematicipGenericEntity(Entity): return unique_id @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon.""" for attr, icon in DEVICE_ATTRIBUTE_ICONS.items(): if getattr(self._device, attr, None): @@ -232,7 +234,7 @@ class HomematicipGenericEntity(Entity): return None @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the generic entity.""" state_attr = {} diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 151807391b9..5ad4efed1f6 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType -from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN +from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN, PLATFORMS from .errors import HmipcConnectionError _LOGGER = logging.getLogger(__name__) @@ -102,10 +102,10 @@ class HomematicipHAP: "Connected to HomematicIP with HAP %s", self.config_entry.unique_id ) - for component in COMPONENTS: + for platform in PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( - self.config_entry, component + self.config_entry, platform ) ) return True @@ -215,9 +215,9 @@ class HomematicipHAP: self._retry_task.cancel() await self.home.disable_events() _LOGGER.info("Closed connection to HomematicIP cloud server") - for component in COMPONENTS: + for platform in PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, component + self.config_entry, platform ) self.hmip_device_by_entity_id = {} return True diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 1909ff818b9..5732ea1bf96 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,5 +1,7 @@ """Support for HomematicIP Cloud lights.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any from homematicip.aio.device import ( AsyncBrandDimmer, @@ -90,9 +92,9 @@ class HomematicipLightMeasuring(HomematicipLight): """Representation of the HomematicIP measuring light.""" @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the light.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes current_power_w = self._device.currentPowerConsumption if current_power_w > 0.05: @@ -206,9 +208,9 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the notification light sensor.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes if self.is_on: state_attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 93e96267be3..f247a58f364 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": ["homematicip==0.13.1"], - "codeowners": ["@SukramJ"], + "codeowners": [], "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 9e202302c10..9e6e96232b4 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,5 +1,7 @@ """Support for HomematicIP Cloud sensors.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -24,6 +26,7 @@ from homematicip.aio.device import ( ) from homematicip.base.enums import ValveState +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, @@ -121,7 +124,7 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipAccesspointDutyCycle(HomematicipGenericEntity): +class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity): """Representation of then HomeMaticIP access point.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -144,7 +147,7 @@ class HomematicipAccesspointDutyCycle(HomematicipGenericEntity): return PERCENTAGE -class HomematicipHeatingThermostat(HomematicipGenericEntity): +class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP heating thermostat.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -173,7 +176,7 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity): return PERCENTAGE -class HomematicipHumiditySensor(HomematicipGenericEntity): +class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP humidity sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -196,7 +199,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity): return PERCENTAGE -class HomematicipTemperatureSensor(HomematicipGenericEntity): +class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP thermometer.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -222,9 +225,9 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity): return TEMP_CELSIUS @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the windspeed sensor.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes temperature_offset = getattr(self._device, "temperatureOffset", None) if temperature_offset: @@ -233,7 +236,7 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity): return state_attr -class HomematicipIlluminanceSensor(HomematicipGenericEntity): +class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP Illuminance sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -259,9 +262,9 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity): return LIGHT_LUX @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the wind speed sensor.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes for attr, attr_key in ILLUMINATION_DEVICE_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -271,7 +274,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity): return state_attr -class HomematicipPowerSensor(HomematicipGenericEntity): +class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP power measuring sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -294,7 +297,7 @@ class HomematicipPowerSensor(HomematicipGenericEntity): return POWER_WATT -class HomematicipWindspeedSensor(HomematicipGenericEntity): +class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP wind speed sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -312,9 +315,9 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity): return SPEED_KILOMETERS_PER_HOUR @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the wind speed sensor.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes wind_direction = getattr(self._device, "windDirection", None) if wind_direction is not None: @@ -327,7 +330,7 @@ class HomematicipWindspeedSensor(HomematicipGenericEntity): return state_attr -class HomematicipTodayRainSensor(HomematicipGenericEntity): +class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP rain counter of a day sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: @@ -345,7 +348,7 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity): return LENGTH_MILLIMETERS -class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity): +class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP passage detector delta counter.""" @property @@ -354,9 +357,9 @@ class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity): return self._device.leftRightCounterDelta @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the delta counter.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes state_attr[ATTR_LEFT_COUNTER] = self._device.leftCounter state_attr[ATTR_RIGHT_COUNTER] = self._device.rightCounter diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 7c92ac5e721..aa82e72e284 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -1,7 +1,8 @@ """Support for HomematicIP Cloud devices.""" +from __future__ import annotations + import logging from pathlib import Path -from typing import Optional from homematicip.aio.device import AsyncSwitchMeasuring from homematicip.aio.group import AsyncHeatingGroup @@ -342,7 +343,7 @@ async def _async_reset_energy_counter( await device.reset_energy_counter() -def _get_home(hass: HomeAssistantType, hapid: str) -> Optional[AsyncHome]: +def _get_home(hass: HomeAssistantType, 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 f8c37d336d5..8172d64d357 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,5 +1,7 @@ """Support for HomematicIP Cloud switches.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -141,9 +143,9 @@ class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): return True @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the switch-group.""" - state_attr = super().device_state_attributes + state_attr = super().extra_state_attributes if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index 1ae318c45ad..eaa8d8834c3 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "A hozz\u00e1f\u00e9r\u00e9si pontot m\u00e1r konfigur\u00e1ltuk", - "connection_aborted": "Nem siker\u00fclt csatlakozni a HMIP szerverhez", - "unknown": "Unknown error occurred." + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "connection_aborted": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "invalid_sgtin_or_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", + "invalid_sgtin_or_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d, pr\u00f3b\u00e1lkozz \u00fajra.", "press_the_button": "Nyomd meg a k\u00e9k gombot.", "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra.", "timeout_button": "K\u00e9k gomb megnyom\u00e1s\u00e1nak id\u0151t\u00fall\u00e9p\u00e9se, pr\u00f3b\u00e1lkozz \u00fajra." @@ -16,7 +16,7 @@ "data": { "hapid": "Hozz\u00e1f\u00e9r\u00e9si pont azonos\u00edt\u00f3ja (SGTIN)", "name": "N\u00e9v (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)", - "pin": "Pin k\u00f3d (opcion\u00e1lis)" + "pin": "PIN-k\u00f3d" }, "title": "V\u00e1lassz HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" }, diff --git a/homeassistant/components/homematicip_cloud/translations/id.json b/homeassistant/components/homematicip_cloud/translations/id.json index 43525955c36..26679d3b37f 100644 --- a/homeassistant/components/homematicip_cloud/translations/id.json +++ b/homeassistant/components/homematicip_cloud/translations/id.json @@ -1,28 +1,28 @@ { "config": { "abort": { - "already_configured": "Jalur akses sudah dikonfigurasi", - "connection_aborted": "Tidak dapat terhubung ke server HMIP", - "unknown": "Kesalahan tidak dikenal terjadi." + "already_configured": "Perangkat sudah dikonfigurasi", + "connection_aborted": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { - "invalid_sgtin_or_pin": "PIN tidak valid, silakan coba lagi.", - "press_the_button": "Silakan tekan tombol biru.", - "register_failed": "Gagal mendaftar, silakan coba lagi.", - "timeout_button": "Batas waktu tekan tombol biru berakhir, silakan coba lagi." + "invalid_sgtin_or_pin": "SGTIN atau Kode PIN tidak valid, coba lagi.", + "press_the_button": "Tekan tombol biru.", + "register_failed": "Gagal mendaftar, coba lagi.", + "timeout_button": "Tenggang waktu penekanan tombol biru berakhir, coba lagi." }, "step": { "init": { "data": { "hapid": "Titik akses ID (SGTIN)", - "name": "Nama (opsional, digunakan sebagai awalan nama untuk semua perangkat)", - "pin": "Kode Pin (opsional)" + "name": "Nama (opsional, digunakan sebagai prefiks nama untuk semua perangkat)", + "pin": "Kode PIN" }, - "title": "Pilih HomematicIP Access point" + "title": "Pilih Access Point HomematicIP" }, "link": { - "description": "Tekan tombol biru pada access point dan tombol submit untuk mendaftarkan HomematicIP dengan rumah asisten.\n\n! [Lokasi tombol di bridge] (/ static/images/config_flows/config_homematicip_cloud.png)", - "title": "Tautkan jalur akses" + "description": "Tekan tombol biru pada access point dan tombol sukirimbmit untuk mendaftarkan HomematicIP dengan Home Assistant.\n\n![Lokasi tombol di bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Tautkan Titik akses" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json index 6a15b21de84..07f6c5c70fc 100644 --- a/homeassistant/components/homematicip_cloud/translations/ko.json +++ b/homeassistant/components/homematicip_cloud/translations/ko.json @@ -6,7 +6,7 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_sgtin_or_pin": "\uc798\ubabb\ub41c SGTIN \uc774\uac70\ub098 PIN \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_sgtin_or_pin": "SGTIN \ub610\ub294 PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "press_the_button": "\ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "register_failed": "\ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "timeout_button": "\uc815\ud574\uc9c4 \uc2dc\uac04\ub0b4\uc5d0 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub974\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." @@ -15,13 +15,13 @@ "init": { "data": { "hapid": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 ID (SGTIN)", - "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uae30\uae30 \uc774\ub984\uc758 \uc811\ub450\uc5b4\ub85c \uc0ac\uc6a9)", + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d, \ubaa8\ub4e0 \uae30\uae30 \uc774\ub984\uc758 \uc811\ub450\uc0ac\ub85c \uc0ac\uc6a9)", "pin": "PIN \ucf54\ub4dc" }, "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd\ud558\uae30" }, "link": { - "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \ud655\uc778\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Home Assistant\uc5d0 HomematicIP\ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \ud655\uc778\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", "title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json index 7127b5c5aae..cb65dee7bd1 100644 --- a/homeassistant/components/homematicip_cloud/translations/nl.json +++ b/homeassistant/components/homematicip_cloud/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Accesspoint is al geconfigureerd", - "connection_aborted": "Kon geen verbinding maken met de HMIP-server", - "unknown": "Er is een onbekende fout opgetreden." + "already_configured": "Apparaat is al geconfigureerd", + "connection_aborted": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" }, "error": { - "invalid_sgtin_or_pin": "Ongeldige PIN-code, probeer het nogmaals.", + "invalid_sgtin_or_pin": "Ongeldige SGTIN of PIN-code, probeer het opnieuw.", "press_the_button": "Druk op de blauwe knop.", "register_failed": "Kan niet registreren, gelieve opnieuw te proberen.", "timeout_button": "Blauwe knop druk op timeout, probeer het opnieuw." @@ -15,8 +15,8 @@ "init": { "data": { "hapid": "Accesspoint ID (SGTIN)", - "name": "Naam (optioneel, gebruikt als naamprefix voor alle apparaten)", - "pin": "Pin-Code (optioneel)" + "name": "Naam (optioneel, gebruikt als naamvoorvoegsel voor alle apparaten)", + "pin": "PIN-code" }, "title": "Kies HomematicIP accesspoint" }, diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index a5a3b9ed077..f62477148f7 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -82,7 +82,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Supported attributes.""" return {"homeworks_address": self._addr} diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 4b87350aec8..8053ad85502 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -1,7 +1,9 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" +from __future__ import annotations + import datetime import logging -from typing import Any, Dict, List, Optional +from typing import Any import requests import somecomfort @@ -192,12 +194,12 @@ class HoneywellUSThermostat(ClimateEntity): self._supported_features |= SUPPORT_FAN_MODE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the honeywell, if any.""" return self._device.name @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" data = {} data[ATTR_FAN_ACTION] = "running" if self._device.fan_running else "idle" @@ -235,7 +237,7 @@ class HoneywellUSThermostat(ClimateEntity): return TEMP_CELSIUS if self._device.temperature_unit == "C" else TEMP_FAHRENHEIT @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> int | None: """Return the current humidity.""" return self._device.current_humidity @@ -245,24 +247,24 @@ class HoneywellUSThermostat(ClimateEntity): return HW_MODE_TO_HVAC_MODE[self._device.system_mode] @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return list(self._hvac_mode_map) @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" if self.hvac_mode == HVAC_MODE_OFF: return None return HW_MODE_TO_HA_HVAC_ACTION[self._device.equipment_output_status] @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.current_temperature @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_COOL: return self._device.setpoint_cool @@ -271,41 +273,41 @@ class HoneywellUSThermostat(ClimateEntity): return None @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: return self._device.setpoint_cool return None @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: return self._device.setpoint_heat return None @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" return PRESET_AWAY if self._away else None @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return [PRESET_NONE, PRESET_AWAY] @property - def is_aux_heat(self) -> Optional[str]: + def is_aux_heat(self) -> str | None: """Return true if aux heater.""" return self._device.system_mode == "emheat" @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting.""" return HW_FAN_MODE_TO_HA[self._device.fan_mode] @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" return list(self._fan_mode_map) diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 5b9fb656938..e6eb211206d 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -184,9 +184,7 @@ class HorizonDevice(MediaPlayerEntity): elif channel: self._client.select_channel(channel) except OSError as msg: - _LOGGER.error( - "%s disconnected: %s. Trying to reconnect...", self._name, msg - ) + _LOGGER.error("%s disconnected: %s. Trying to reconnect", self._name, msg) # for reconnect, first gracefully disconnect self._client.disconnect() diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index da597acb8b7..297bfa5264f 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -5,7 +5,7 @@ import logging import hpilo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -18,7 +18,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -100,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class HpIloSensor(Entity): +class HpIloSensor(SensorEntity): """Representation of a HP iLO sensor.""" def __init__( @@ -144,7 +143,7 @@ class HpIloSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._state_attributes diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 33dd8118ee4..b3d5a081d1b 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -1,4 +1,5 @@ """HTML5 Push Messaging notification service.""" +from contextlib import suppress from datetime import datetime, timedelta from functools import partial import json @@ -202,10 +203,8 @@ def get_service(hass, config, discovery_info=None): def _load_config(filename): """Load configuration.""" - try: + with suppress(HomeAssistantError): return load_json(filename) - except HomeAssistantError: - pass return {} @@ -325,10 +324,8 @@ class HTML5PushCallbackView(HomeAssistantView): if target_check.get(ATTR_TARGET) in self.registrations: possible_target = self.registrations[target_check[ATTR_TARGET]] key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH] - try: + with suppress(jwt.exceptions.DecodeError): return jwt.decode(token, key, algorithms=["ES256", "HS256"]) - except jwt.exceptions.DecodeError: - pass return self.json_message( "No target found in JWT", status_code=HTTP_UNAUTHORIZED diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 993d466ae18..5f57b4b77b8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,10 +1,12 @@ """Support to serve the Home Assistant API as WSGI application.""" +from __future__ import annotations + from contextvars import ContextVar from ipaddress import ip_network import logging import os import ssl -from typing import Dict, Optional, cast +from typing import Optional, cast from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently @@ -102,7 +104,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) @bind_hass -async def async_get_last_config(hass: HomeAssistant) -> Optional[dict]: +async def async_get_last_config(hass: HomeAssistant) -> dict | None: """Return the last known working config.""" store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) return cast(Optional[dict], await store.async_load()) @@ -115,7 +117,7 @@ class ApiConfig: self, local_ip: str, host: str, - port: Optional[int] = SERVER_PORT, + port: int | None = SERVER_PORT, use_ssl: bool = False, ) -> None: """Initialize a new API config object.""" @@ -379,7 +381,7 @@ class HomeAssistantHTTP: async def start_http_server_and_save_config( - hass: HomeAssistant, conf: Dict, server: HomeAssistantHTTP + hass: HomeAssistant, conf: dict, server: HomeAssistantHTTP ) -> None: """Startup the http server and save the config.""" await server.start() # type: ignore @@ -395,6 +397,6 @@ async def start_http_server_and_save_config( await store.async_save(conf) -current_request: ContextVar[Optional[web.Request]] = ContextVar( +current_request: ContextVar[web.Request | None] = ContextVar( "current_request", default=None ) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 2e51dc35d88..5350ae5d4c8 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -1,10 +1,12 @@ """Ban logic for HTTP component.""" +from __future__ import annotations + from collections import defaultdict +from contextlib import suppress from datetime import datetime from ipaddress import ip_address import logging from socket import gethostbyaddr, herror -from typing import List, Optional from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized @@ -98,12 +100,10 @@ async def process_wrong_login(request): remote_addr = ip_address(request.remote) remote_host = request.remote - try: + with suppress(herror): remote_host, _, _ = await hass.async_add_executor_job( gethostbyaddr, request.remote ) - except herror: - pass base_msg = f"Login attempt or request with invalid authentication from {remote_host} ({remote_addr})." @@ -178,15 +178,15 @@ async def process_success_login(request): class IpBan: """Represents banned IP address.""" - def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None: + def __init__(self, ip_ban: str, banned_at: datetime | None = None) -> None: """Initialize IP Ban object.""" self.ip_address = ip_address(ip_ban) self.banned_at = banned_at or dt_util.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]: +async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> list[IpBan]: """Load list of banned IPs from config file.""" - ip_list: List[IpBan] = [] + ip_list: list[IpBan] = [] try: list_ = await hass.async_add_executor_job(load_yaml_config_file, path) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 354159f13be..b4dbb845638 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -1,8 +1,10 @@ """Support for views.""" +from __future__ import annotations + import asyncio import json import logging -from typing import Any, Callable, List, Optional +from typing import Any, Callable from aiohttp import web from aiohttp.typedefs import LooseHeaders @@ -26,8 +28,8 @@ _LOGGER = logging.getLogger(__name__) class HomeAssistantView: """Base view for all views.""" - url: Optional[str] = None - extra_urls: List[str] = [] + url: str | None = None + extra_urls: list[str] = [] # Views inheriting from this class can override this requires_auth = True cors_allowed = False @@ -45,7 +47,7 @@ class HomeAssistantView: def json( result: Any, status_code: int = HTTP_OK, - headers: Optional[LooseHeaders] = None, + headers: LooseHeaders | None = None, ) -> web.Response: """Return a JSON response.""" try: @@ -66,8 +68,8 @@ class HomeAssistantView: self, message: str, status_code: int = HTTP_OK, - message_code: Optional[str] = None, - headers: Optional[LooseHeaders] = None, + message_code: str | None = None, + headers: LooseHeaders | None = None, ) -> web.Response: """Return a JSON message response.""" data = {"message": message} diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py index c30ba32b780..f3dd59bf9d7 100644 --- a/homeassistant/components/http/web_runner.py +++ b/homeassistant/components/http/web_runner.py @@ -1,7 +1,8 @@ """HomeAssistant specific aiohttp Site.""" +from __future__ import annotations + import asyncio from ssl import SSLContext -from typing import List, Optional, Union from aiohttp import web from yarl import URL @@ -22,18 +23,18 @@ class HomeAssistantTCPSite(web.BaseSite): __slots__ = ("_host", "_port", "_reuse_address", "_reuse_port", "_hosturl") - def __init__( + def __init__( # noqa: D107 self, - runner: "web.BaseRunner", - host: Union[None, str, List[str]], + runner: web.BaseRunner, + host: None | str | list[str], port: int, *, shutdown_timeout: float = 10.0, - ssl_context: Optional[SSLContext] = None, + ssl_context: SSLContext | None = None, backlog: int = 128, - reuse_address: Optional[bool] = None, - reuse_port: Optional[bool] = None, - ) -> None: # noqa: D107 + reuse_address: bool | None = None, + reuse_port: bool | None = None, + ) -> None: super().__init__( runner, shutdown_timeout=shutdown_timeout, diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index a9b235faa0b..d32eebf6d5f 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -4,13 +4,12 @@ from functools import partial import logging from i2csense.htu21d import HTU21D # pylint: disable=import-error -import smbus # pylint: disable=import-error +import smbus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -70,7 +69,7 @@ class HTU21DHandler: self.sensor.update() -class HTU21DSensor(Entity): +class HTU21DSensor(SensorEntity): """Implementation of the HTU21D sensor.""" def __init__(self, htu21d_client, name, variable, unit): diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 341f9c0a118..67170aaf866 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,12 +1,14 @@ """Support for Huawei LTE routers.""" +from __future__ import annotations from collections import defaultdict +from contextlib import suppress from datetime import timedelta from functools import partial import ipaddress import logging import time -from typing import Any, Callable, Dict, List, Set, Tuple, cast +from typing import Any, Callable, cast from urllib.parse import urlparse import attr @@ -138,13 +140,13 @@ class Router: mac: str = attr.ib() signal_update: CALLBACK_TYPE = attr.ib() - data: Dict[str, Any] = attr.ib(init=False, factory=dict) - subscriptions: Dict[str, Set[str]] = attr.ib( + data: dict[str, Any] = attr.ib(init=False, factory=dict) + subscriptions: dict[str, set[str]] = attr.ib( init=False, 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) + 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) @@ -160,14 +162,12 @@ class Router: (KEY_DEVICE_BASIC_INFORMATION, "devicename"), (KEY_DEVICE_INFORMATION, "DeviceName"), ): - try: + with suppress(KeyError, TypeError): return cast(str, self.data[key][item]) - except (KeyError, TypeError): - pass return DEFAULT_DEVICE_NAME @property - def device_identifiers(self) -> Set[Tuple[str, str]]: + def device_identifiers(self) -> set[tuple[str, str]]: """Get router identifiers for device registry.""" try: return {(DOMAIN, self.data[KEY_DEVICE_INFORMATION]["SerialNumber"])} @@ -175,7 +175,7 @@ class Router: return set() @property - def device_connections(self) -> Set[Tuple[str, str]]: + def device_connections(self) -> set[tuple[str, str]]: """Get router connections for device registry.""" return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() @@ -196,14 +196,14 @@ class Router: self.subscriptions.pop(key) except ResponseErrorLoginRequiredException: if isinstance(self.connection, AuthorizedConnection): - _LOGGER.debug("Trying to authorize again...") + _LOGGER.debug("Trying to authorize again") if self.connection.enforce_authorized_connection(): _LOGGER.debug( - "...success, %s will be updated by a future periodic run", + "success, %s will be updated by a future periodic run", key, ) else: - _LOGGER.debug("...failed") + _LOGGER.debug("failed") return _LOGGER.info( "%s requires authorization, excluding from future updates", key @@ -304,8 +304,8 @@ class HuaweiLteData: hass_config: dict = attr.ib() # Our YAML config, keyed by router URL - config: Dict[str, Dict[str, Any]] = attr.ib() - routers: Dict[str, Router] = attr.ib(init=False, factory=dict) + config: dict[str, dict[str, Any]] = attr.ib() + routers: dict[str, Router] = attr.ib(init=False, factory=dict) async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: @@ -484,7 +484,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: logging.getLogger("dicttoxml").setLevel(logging.WARNING) # Arrange our YAML config to dict with normalized URLs as keys - domain_config: Dict[str, Dict[str, Any]] = {} + domain_config: dict[str, dict[str, Any]] = {} if DOMAIN not in hass.data: hass.data[DOMAIN] = HuaweiLteData(hass_config=config, config=domain_config) for router_config in config.get(DOMAIN, []): @@ -588,7 +588,7 @@ class HuaweiLteBaseEntity(Entity): router: Router = attr.ib() _available: bool = attr.ib(init=False, default=True) - _unsub_handlers: List[Callable] = attr.ib(init=False, factory=list) + _unsub_handlers: list[Callable] = attr.ib(init=False, factory=list) @property def _entity_name(self) -> str: @@ -620,7 +620,7 @@ class HuaweiLteBaseEntity(Entity): return False @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Get info for matching with parent router.""" return { "identifiers": self.router.device_identifiers, @@ -636,7 +636,6 @@ class HuaweiLteBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Connect to update signals.""" - assert self.hass is not None self._unsub_handlers.append( async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) ) diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 9a5f148d138..833a632b0db 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -1,7 +1,8 @@ """Support for Huawei LTE binary sensors.""" +from __future__ import annotations import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable import attr from huawei_lte_api.enums.cradle import ConnectionStatusEnum @@ -29,11 +30,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] - entities: List[Entity] = [] + entities: list[Entity] = [] if router.data.get(KEY_MONITORING_STATUS): entities.append(HuaweiLteMobileConnectionBinarySensor(router)) @@ -53,7 +54,7 @@ class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorEntity): key: str item: str - _raw_state: Optional[str] = attr.ib(init=False, default=None) + _raw_state: str | None = attr.ib(init=False, default=None) @property def entity_registry_enabled_default(self) -> bool: @@ -142,12 +143,10 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): return True @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Get additional attributes related to connection status.""" - attributes = super().device_state_attributes + attributes = {} if self._raw_state in CONNECTION_STATE_ATTRIBUTES: - if attributes is None: - attributes = {} attributes["additional_state"] = CONNECTION_STATE_ATTRIBUTES[ self._raw_state ] diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index e38b873a5bb..415e2ea2bc3 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import OrderedDict import logging -from typing import Any, Dict, Optional +from typing import Any from urllib.parse import urlparse from huawei_lte_api.AuthorizedConnection import AuthorizedConnection @@ -31,10 +31,12 @@ from homeassistant.const import ( ) from homeassistant.core import callback -from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME - -# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue -from .const import DOMAIN # pylint: disable=unused-import +from .const import ( + CONNECTION_TIMEOUT, + DEFAULT_DEVICE_NAME, + DEFAULT_NOTIFY_SERVICE_NAME, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -55,9 +57,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_show_user_form( self, - user_input: Optional[Dict[str, Any]] = None, - errors: Optional[Dict[str, str]] = None, - ) -> Dict[str, Any]: + user_input: dict[str, Any] | None = None, + errors: dict[str, str] | None = None, + ) -> dict[str, Any]: if user_input is None: user_input = {} return self.async_show_form( @@ -94,12 +96,12 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_import( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle import initiated config flow.""" return await self.async_step_user(user_input) - def _already_configured(self, user_input: Dict[str, Any]) -> bool: + def _already_configured(self, user_input: dict[str, Any]) -> bool: """See if we already have a router matching user input configured.""" existing_urls = { url_normalize(entry.data[CONF_URL], default_scheme="http") @@ -108,8 +110,8 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return user_input[CONF_URL] in existing_urls async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle user initiated config flow.""" if user_input is None: return await self._async_show_user_form() @@ -129,7 +131,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self._already_configured(user_input): return self.async_abort(reason="already_configured") - conn: Optional[Connection] = None + conn: Connection | None = None def logout() -> None: if isinstance(conn, AuthorizedConnection): @@ -138,7 +140,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.debug("Could not logout", exc_info=True) - def try_connect(user_input: Dict[str, Any]) -> Connection: + def try_connect(user_input: dict[str, Any]) -> Connection: """Try connecting with given credentials.""" username = user_input.get(CONF_USERNAME) password = user_input.get(CONF_PASSWORD) @@ -222,8 +224,8 @@ 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]: + self, discovery_info: dict[str, Any] + ) -> dict[str, Any]: """Handle SSDP initiated config flow.""" await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() @@ -263,8 +265,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry async def async_step_init( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle options flow.""" # Recipients are persisted as a list, but handled as comma separated string in UI diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 52e59b713dd..b042c0c2912 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,8 +1,9 @@ """Support for device tracking of Huawei LTE routers.""" +from __future__ import annotations import logging import re -from typing import Any, Callable, Dict, List, Optional, Set, cast +from typing import Any, Callable, cast import attr from stringcase import snakecase @@ -31,7 +32,7 @@ _DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up from config entry.""" @@ -46,9 +47,9 @@ async def async_setup_entry( return # Initialize already tracked entities - tracked: Set[str] = set() + tracked: set[str] = set() registry = await entity_registry.async_get_registry(hass) - known_entities: List[Entity] = [] + known_entities: list[Entity] = [] for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN @@ -82,8 +83,8 @@ async def async_setup_entry( def async_add_new_entities( hass: HomeAssistantType, router_url: str, - async_add_entities: Callable[[List[Entity], bool], None], - tracked: Set[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] @@ -93,7 +94,7 @@ def async_add_new_entities( _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") return - new_entities: List[Entity] = [] + new_entities: list[Entity] = [] for host in (x for x in hosts if x.get("MacAddress")): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) if entity.unique_id in tracked: @@ -125,12 +126,12 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): mac: str = attr.ib() _is_connected: bool = attr.ib(init=False, default=False) - _hostname: Optional[str] = attr.ib(init=False, default=None) - _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + _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._device_state_attributes["mac_address"] = self.mac + self._extra_state_attributes["mac_address"] = self.mac @property def _entity_name(self) -> str: @@ -151,9 +152,9 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): return self._is_connected @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Get additional attributes related to entity state.""" - return self._device_state_attributes + return self._extra_state_attributes async def async_update(self) -> None: """Update state.""" @@ -162,6 +163,6 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): self._is_connected = host is not None if host is not None: self._hostname = host.get("HostName") - self._device_state_attributes = { + self._extra_state_attributes = { _better_snakecase(k): v for k, v in host.items() if k != "HostName" } diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index ef354fefaf3..ea7b5d9f6ab 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging import time -from typing import Any, Dict, List, Optional +from typing import Any import attr from huawei_lte_api.exceptions import ResponseErrorException @@ -20,9 +20,9 @@ _LOGGER = logging.getLogger(__name__) async def async_get_service( hass: HomeAssistantType, - config: Dict[str, Any], - discovery_info: Optional[Dict[str, Any]] = None, -) -> Optional[HuaweiLteSmsNotificationService]: + config: dict[str, Any], + discovery_info: dict[str, Any] | None = None, +) -> HuaweiLteSmsNotificationService | None: """Get the notification service.""" if discovery_info is None: return None @@ -38,7 +38,7 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): """Huawei LTE router SMS notification service.""" router: Router = attr.ib() - default_targets: List[str] = attr.ib() + default_targets: list[str] = attr.ib() def send_message(self, message: str = "", **kwargs: Any) -> None: """Send message to target numbers.""" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 3815ac831b5..c6cb93f0e67 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -1,9 +1,10 @@ """Support for Huawei LTE sensors.""" +from __future__ import annotations from bisect import bisect import logging import re -from typing import Callable, Dict, List, NamedTuple, Optional, Pattern, Tuple, Union +from typing import Callable, NamedTuple, Pattern import attr @@ -11,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, + SensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -45,17 +47,17 @@ _LOGGER = logging.getLogger(__name__) class SensorMeta(NamedTuple): """Metadata for defining sensors.""" - name: Optional[str] = None - device_class: Optional[str] = None - icon: Union[str, Callable[[StateType], str], None] = None - unit: Optional[str] = None + name: str | None = None + device_class: str | None = None + icon: str | Callable[[StateType], str] | None = None + unit: str | None = None enabled_default: bool = False - include: Optional[Pattern[str]] = None - exclude: Optional[Pattern[str]] = None - formatter: Optional[Callable[[str], Tuple[StateType, Optional[str]]]] = None + include: Pattern[str] | None = None + exclude: Pattern[str] | None = None + formatter: Callable[[str], tuple[StateType, str | None]] | None = None -SENSOR_META: Dict[Union[str, Tuple[str, str]], SensorMeta] = { +SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { KEY_DEVICE_INFORMATION: SensorMeta( include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) ), @@ -329,11 +331,11 @@ SENSOR_META: Dict[Union[str, Tuple[str, str]], SensorMeta] = { async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] - sensors: List[Entity] = [] + sensors: list[Entity] = [] for key in SENSOR_KEYS: items = router.data.get(key) if not items: @@ -354,7 +356,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -def format_default(value: StateType) -> Tuple[StateType, Optional[str]]: +def format_default(value: StateType) -> tuple[StateType, str | None]: """Format value.""" unit = None if value is not None: @@ -372,7 +374,7 @@ def format_default(value: StateType) -> Tuple[StateType, Optional[str]]: @attr.s -class HuaweiLteSensor(HuaweiLteBaseEntity): +class HuaweiLteSensor(HuaweiLteBaseEntity, SensorEntity): """Huawei LTE sensor entity.""" key: str = attr.ib() @@ -380,7 +382,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntity): meta: SensorMeta = attr.ib() _state: StateType = attr.ib(init=False, default=STATE_UNKNOWN) - _unit: Optional[str] = attr.ib(init=False) + _unit: str | None = attr.ib(init=False) async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" @@ -406,17 +408,17 @@ class HuaweiLteSensor(HuaweiLteBaseEntity): return self._state @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return sensor device class.""" return self.meta.device_class @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return sensor's unit of measurement.""" return self.meta.unit or self._unit @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return icon for sensor.""" icon = self.meta.icon if callable(icon): diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 4dfa1e32df2..9279226e8ec 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -1,7 +1,8 @@ """Support for Huawei LTE switches.""" +from __future__ import annotations import logging -from typing import Any, Callable, List, Optional +from typing import Any, Callable import attr @@ -24,11 +25,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up from config entry.""" router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] - switches: List[Entity] = [] + switches: list[Entity] = [] if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) @@ -42,7 +43,7 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchEntity): key: str item: str - _raw_state: Optional[str] = attr.ib(init=False, default=None) + _raw_state: str | None = attr.ib(init=False, default=None) def _turn(self, state: bool) -> None: raise NotImplementedError diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 90814fea5a5..815794133d2 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -1,20 +1,24 @@ { "config": { "abort": { - "already_configured": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" }, "error": { "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9se", "incorrect_password": "Hib\u00e1s jelsz\u00f3", "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", - "invalid_url": "\u00c9rv\u00e9nytelen URL" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_url": "\u00c9rv\u00e9nytelen URL", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { "password": "Jelsz\u00f3", + "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "title": "Huawei LTE konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json new file mode 100644 index 00000000000..2077b31ccd7 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "not_huawei_lte": "Bukan perangkat Huawei LTE" + }, + "error": { + "connection_timeout": "Tenggang waktu terhubung habis", + "incorrect_password": "Kata sandi salah", + "incorrect_username": "Nama pengguna salah", + "invalid_auth": "Autentikasi tidak valid", + "invalid_url": "URL tidak valid", + "login_attempts_exceeded": "Upaya login maksimum telah terlampaui, coba lagi nanti", + "response_error": "Kesalahan tidak dikenal dari perangkat", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Huawei LTE: {name}", + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + }, + "description": "Masukkan detail akses perangkat. Menentukan nama pengguna dan kata sandi bersifat opsional, tetapi memungkinkan dukungan untuk fitur integrasi lainnya. Selain itu, penggunaan koneksi resmi dapat menyebabkan masalah mengakses antarmuka web perangkat dari luar Home Assistant saat integrasi aktif, dan sebaliknya.", + "title": "Konfigurasikan Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nama layanan notifikasi (perubahan harus dimulai ulang)", + "recipient": "Penerima notifikasi SMS", + "track_new_devices": "Lacak perangkat baru" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 73274d15bfb..ab108964ed8 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -23,7 +23,7 @@ "url": "URL \uc8fc\uc18c", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\uae30\uae30 \uc811\uadfc\uc5d0 \ub300\ud55c \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant\uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc811\uadfc\ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "Huawei LTE \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index d420093996c..799a9ce50af 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Dit apparaat is reeds geconfigureerd", - "already_in_progress": "Dit apparaat wordt al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "not_huawei_lte": "Geen Huawei LTE-apparaat" }, "error": { @@ -15,6 +15,7 @@ "response_error": "Onbekende fout van het apparaat", "unknown": "Onverwachte fout" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index c2ec20fb259..d3f95e3fbf1 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -8,7 +8,7 @@ "error": { "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", - "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", + "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\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.", "login_attempts_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", @@ -21,9 +21,9 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "url": "URL-\u0430\u0434\u0440\u0435\u0441", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.", "title": "Huawei LTE" } } diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index cfbe041aafe..e408e995ad4 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -31,9 +31,9 @@ class HuePresence(GenericZLLSensor, BinarySensorEntity): return self.sensor.presence @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - attributes = super().device_state_attributes + attributes = super().extra_state_attributes if "sensitivity" in self.sensor.config: attributes["sensitivity"] = self.sensor.config["sensitivity"] if "sensitivitymax" in self.sensor.config: diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index dc9b56fcdfe..c14caa89620 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -252,7 +252,7 @@ class HueBridge: # we already created a new config flow, no need to do it again return LOGGER.error( - "Unable to authorize to bridge %s, setup the linking again.", self.host + "Unable to authorize to bridge %s, setup the linking again", self.host ) self.authorized = False create_config_flow(self.hass, self.host) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 580b69251c2..9fd025d7b6a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure Philips Hue.""" +from __future__ import annotations + import asyncio -from typing import Any, Dict, Optional +from typing import Any from urllib.parse import urlparse import aiohue @@ -15,7 +17,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge -from .const import ( # pylint: disable=unused-import +from .const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, @@ -25,7 +27,7 @@ from .const import ( # pylint: disable=unused-import ) from .errors import AuthenticationRequired, CannotConnect -HUE_MANUFACTURERURL = "http://www.philips.com" +HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com") HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] HUE_MANUAL_BRIDGE_ID = "manual" @@ -44,8 +46,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Hue flow.""" - self.bridge: Optional[aiohue.Bridge] = None - self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None + self.bridge: aiohue.Bridge | None = None + self.discovered_bridges: dict[str, aiohue.Bridge] | None = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -53,7 +55,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_init(user_input) @core.callback - def _async_get_bridge(self, host: str, bridge_id: Optional[str] = None): + def _async_get_bridge(self, host: str, bridge_id: str | None = None): """Return a bridge object.""" if bridge_id is not None: bridge_id = normalize_bridge_id(bridge_id) @@ -114,8 +116,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_manual( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle manual bridge setup.""" if user_input is None: return self.async_show_form( @@ -179,7 +181,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host is already configured and delegate to the import step if not. """ # Filter out non-Hue bridges #1 - if discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) != HUE_MANUFACTURERURL: + if ( + discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) + not in HUE_MANUFACTURERURL + ): return self.async_abort(reason="not_hue_bridge") # Filter out non-Hue bridges #2 @@ -207,6 +212,17 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge = bridge return await self.async_step_link() + async def async_step_homekit(self, discovery_info): + """Handle a discovered Hue bridge on HomeKit. + + The bridge ID communicated over HomeKit differs, so we cannot use that + as the unique identifier. Therefore, this method uses discovery without + a unique ID. + """ + self.bridge = self._async_get_bridge(discovery_info[CONF_HOST]) + await self._async_handle_discovery_without_unique_id() + return await self.async_step_link() + async def async_step_import(self, import_info): """Import a new bridge as a config entry. @@ -235,8 +251,8 @@ class HueOptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry async def async_step_init( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 6384e47b45e..3d193734005 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -229,7 +229,7 @@ async def async_safe_fetch(bridge, fetch_method): except aiohue.Unauthorized as err: await bridge.handle_unauthorized_error() raise UpdateFailed("Unauthorized") from err - except (aiohue.AiohueException,) as err: + except aiohue.AiohueException as err: raise UpdateFailed(f"Hue error: {err}") from err @@ -297,12 +297,11 @@ class HueLight(CoordinatorEntity, LightEntity): "bulb in the Philips Hue App." ) _LOGGER.warning(err, self.name) - if self.gamut: - if 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)) - self.gamut_typ = GAMUT_TYPE_UNAVAILABLE - self.gamut = None + 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)) + self.gamut_typ = GAMUT_TYPE_UNAVAILABLE + self.gamut = None @property def unique_id(self): @@ -535,7 +534,7 @@ class HueLight(CoordinatorEntity, LightEntity): await self.coordinator.async_request_refresh() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if not self.is_group: return {} diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index f5911bbb50c..6ac8d134327 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -6,6 +6,7 @@ from aiohue.sensors import ( TYPE_ZLL_TEMPERATURE, ) +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, @@ -14,7 +15,6 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from .const import DOMAIN as HUE_DOMAIN from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor @@ -31,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ].sensor_manager.async_register_component("sensor", async_add_entities) -class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity): +class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): """Parent class for all 'gauge' Hue device sensors.""" async def _async_update_ha_state(self, *args, **kwargs): @@ -58,9 +58,9 @@ class HueLightLevel(GenericHueGaugeSensorEntity): return round(float(10 ** ((self.sensor.lightlevel - 1) / 10000)), 2) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - attributes = super().device_state_attributes + attributes = super().extra_state_attributes attributes.update( { "lightlevel": self.sensor.lightlevel, @@ -88,7 +88,7 @@ class HueTemperature(GenericHueGaugeSensorEntity): return self.sensor.temperature / 100 -class HueBattery(GenericHueSensor): +class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" @property diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 263140464aa..9f764e04d28 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -197,6 +197,6 @@ class GenericZLLSensor(GenericHueSensor): """Representation of a Hue-brand, physical sensor.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return {"battery_level": self.sensor.battery} diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index c3e3203a66c..d0aa043b10b 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -2,14 +2,16 @@ "config": { "abort": { "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", - "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", + "not_hue_bridge": "Nem egy Hue Bridge", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" }, "error": { - "linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.", + "linking": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" }, "step": { @@ -22,6 +24,12 @@ "link": { "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" + }, + "manual": { + "data": { + "host": "Hoszt" + }, + "title": "A Hue bridge manu\u00e1lis konfigur\u00e1l\u00e1sa" } } }, @@ -29,6 +37,10 @@ "trigger_subtype": { "turn_off": "Kikapcsol\u00e1s", "turn_on": "Bekapcsol\u00e1s" + }, + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" gomb lenyomva", + "remote_button_short_release": "\"{subtype}\" gomb elengedve" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index 2af5753f654..c9e0bcd75d4 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -1,27 +1,66 @@ { "config": { "abort": { - "all_configured": "Semua Philips Hue bridges sudah dikonfigurasi", - "already_configured": "Bridge sudah dikonfigurasi", - "cannot_connect": "Tidak dapat terhubung ke bridge", - "discover_timeout": "Tidak dapat menemukan Hue Bridges.", + "all_configured": "Semua bridge Philips Hue sudah dikonfigurasi", + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "discover_timeout": "Tidak dapat menemukan bridge Hue", "no_bridges": "Bridge Philips Hue tidak ditemukan", - "unknown": "Kesalahan tidak dikenal terjadi." + "not_hue_bridge": "Bukan bridge Hue", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { - "linking": "Terjadi kesalahan tautan tidak dikenal.", - "register_failed": "Gagal mendaftar, silakan coba lagi." + "linking": "Kesalahan yang tidak diharapkan", + "register_failed": "Gagal mendaftar, coba lagi." }, "step": { "init": { "data": { "host": "Host" }, - "title": "Pilih Hue bridge" + "title": "Pilih bridge Hue" }, "link": { - "description": "Tekan tombol di bridge untuk mendaftar Philips Hue dengan Home Assistant.\n\n![Lokasi tombol di bridge](/static/images/config_philips_hue.jpg)", - "title": "Tautan Hub" + "description": "Tekan tombol di bridge untuk mendaftarkan Philips Hue dengan Home Assistant.\n\n![Lokasi tombol di bridge](/static/images/config_philips_hue.jpg)", + "title": "Tautkan Hub" + }, + "manual": { + "data": { + "host": "Host" + }, + "title": "Konfigurasi bridge Hue secara manual" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Tombol pertama", + "button_2": "Tombol kedua", + "button_3": "Tombol ketiga", + "button_4": "Tombol keempat", + "dim_down": "Redupkan", + "dim_up": "Terangkan", + "double_buttons_1_3": "Tombol Pertama dan Ketiga", + "double_buttons_2_4": "Tombol Kedua dan Keempat", + "turn_off": "Matikan", + "turn_on": "Nyalakan" + }, + "trigger_type": { + "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_short_press": "Tombol \"{subtype}\" ditekan", + "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", + "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama", + "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Izinkan grup Hue", + "allow_unreachable": "Izinkan bohlam yang tidak dapat dijangkau untuk melaporkan statusnya dengan benar" + } } } } diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json index 846ea937515..30da5cee484 100644 --- a/homeassistant/components/hue/translations/ko.json +++ b/homeassistant/components/hue/translations/ko.json @@ -47,11 +47,11 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", - "remote_double_button_long_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", - "remote_double_button_short_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uc190\uc744 \ub5c4 \ub54c" + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c", + "remote_double_button_long_press": "\ub450 \"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c", + "remote_double_button_short_press": "\ub450 \"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c" } }, "options": { diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index cead9dd21c6..0938c18e1ea 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -2,16 +2,16 @@ "config": { "abort": { "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", - "already_configured": "Bridge is al geconfigureerd", - "already_in_progress": "De configuratiestroom voor het apparaat is al in volle gang.", - "cannot_connect": "Kan geen verbinding maken met bridge", + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken", "discover_timeout": "Hue bridges kunnen niet worden gevonden", "no_bridges": "Geen Philips Hue bridges ontdekt", "not_hue_bridge": "Dit is geen Hue bridge", "unknown": "Onverwachte fout" }, "error": { - "linking": "Er is een onbekende verbindingsfout opgetreden.", + "linking": "Onverwachte fout", "register_failed": "Registratie is mislukt, probeer het opnieuw" }, "step": { @@ -28,7 +28,8 @@ "manual": { "data": { "host": "Host" - } + }, + "title": "Handmatig een Hue bridge configureren" } } }, @@ -52,5 +53,15 @@ "remote_double_button_long_press": "Beide \"{subtype}\" losgelaten na lang indrukken", "remote_double_button_short_press": "Beide \"{subtype}\" losgelaten" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Sta Hue-groepen toe", + "allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/zh-Hans.json b/homeassistant/components/hue/translations/zh-Hans.json index c34bd68aad8..1dcc6b8f59b 100644 --- a/homeassistant/components/hue/translations/zh-Hans.json +++ b/homeassistant/components/hue/translations/zh-Hans.json @@ -29,6 +29,13 @@ "device_automation": { "trigger_subtype": { "turn_off": "\u5173\u95ed" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", + "remote_button_short_press": "\"{subtype}\" \u5355\u51fb", + "remote_button_short_release": "\"{subtype}\" \u677e\u5f00", + "remote_double_button_long_press": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u957f\u6309\u540e\u677e\u5f00", + "remote_double_button_short_press": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u677e\u5f00" } } } \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 23dc3cb7eda..3af6db3efb5 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -8,7 +8,6 @@ from huisbaasje import Huisbaasje, HuisbaasjeException 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.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -61,10 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): update_interval=timedelta(seconds=POLLING_INTERVAL), ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() # Load the client in the data of home assistant hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { @@ -100,7 +96,7 @@ async def async_update_huisbaasje(huisbaasje): # handled by the data update coordinator. async with async_timeout.timeout(FETCH_TIMEOUT): if not huisbaasje.is_authenticated(): - _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating...") + _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating") await huisbaasje.authenticate() current_measurements = await huisbaasje.current_measurements() @@ -141,7 +137,7 @@ def _get_cumulative_value( :param source_type: The source of energy (electricity or gas) :param period_type: The period for which cumulative value should be given. """ - if source_type in current_measurements.keys(): + if source_type in current_measurements: if ( period_type in current_measurements[source_type] and current_measurements[source_type][period_type] is not None diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py index 59e4840529d..c8681c31188 100644 --- a/homeassistant/components/huisbaasje/config_flow.py +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import AbortFlow -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,9 +32,18 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: user_id = await self._validate_input(user_input) - - _LOGGER.info("Input for Huisbaasje is valid!") - + except HuisbaasjeConnectionException as exception: + _LOGGER.warning(exception) + errors["base"] = "cannot_connect" + except HuisbaasjeException as exception: + _LOGGER.warning(exception) + errors["base"] = "invalid_auth" + except AbortFlow: + raise + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: # Set user id as unique id await self.async_set_unique_id(user_id) self._abort_if_unique_id_configured() @@ -48,17 +57,6 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_PASSWORD: user_input[CONF_PASSWORD], }, ) - except HuisbaasjeConnectionException as exception: - _LOGGER.warning(exception) - errors["base"] = "connection_exception" - except HuisbaasjeException as exception: - _LOGGER.warning(exception) - errors["base"] = "invalid_auth" - except AbortFlow as exception: - raise exception - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" return await self._show_setup_form(user_input, errors) @@ -72,7 +70,6 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Data has the keys from DATA_SCHEMA with values provided by the user. """ - username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index 07ad84567e5..abac03e6182 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -9,6 +9,7 @@ from huisbaasje.const import ( ) from homeassistant.const import ( + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, TIME_HOURS, @@ -70,34 +71,34 @@ SENSORS_INFO = [ }, { "name": "Huisbaasje Energy Today", + "device_class": DEVICE_CLASS_ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_DAY, - "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Energy This Week", + "device_class": DEVICE_CLASS_ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_WEEK, - "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Energy This Month", + "device_class": DEVICE_CLASS_ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_MONTH, - "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Energy This Year", + "device_class": DEVICE_CLASS_ENERGY, "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "source_type": SOURCE_TYPE_ELECTRICITY, "sensor_type": SENSOR_TYPE_THIS_YEAR, - "icon": "mdi:counter", "precision": 1, }, { diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index e84052fe029..3acf39a140d 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, POWER_WATT from homeassistant.core import HomeAssistant @@ -23,7 +24,7 @@ async def async_setup_entry( ) -class HuisbaasjeSensor(CoordinatorEntity): +class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): """Defines a Huisbaasje sensor.""" def __init__( diff --git a/homeassistant/components/huisbaasje/strings.json b/homeassistant/components/huisbaasje/strings.json index f126ac0afff..169b9a0e901 100644 --- a/homeassistant/components/huisbaasje/strings.json +++ b/homeassistant/components/huisbaasje/strings.json @@ -10,8 +10,7 @@ }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unauthenticated_exception": "[%key:common::config_flow::error::invalid_auth%]", - "connection_exception": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/huisbaasje/translations/bg.json b/homeassistant/components/huisbaasje/translations/bg.json new file mode 100644 index 00000000000..67a484573aa --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/ca.json b/homeassistant/components/huisbaasje/translations/ca.json index 99d99d4340f..3ee45b4c38b 100644 --- a/homeassistant/components/huisbaasje/translations/ca.json +++ b/homeassistant/components/huisbaasje/translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", "connection_exception": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unauthenticated_exception": "Autenticaci\u00f3 inv\u00e0lida", diff --git a/homeassistant/components/huisbaasje/translations/cs.json b/homeassistant/components/huisbaasje/translations/cs.json index 07a1d29330b..8c89c265fe5 100644 --- a/homeassistant/components/huisbaasje/translations/cs.json +++ b/homeassistant/components/huisbaasje/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "connection_exception": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unauthenticated_exception": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", diff --git a/homeassistant/components/huisbaasje/translations/en.json b/homeassistant/components/huisbaasje/translations/en.json index 16832be30e7..42bb4b59196 100644 --- a/homeassistant/components/huisbaasje/translations/en.json +++ b/homeassistant/components/huisbaasje/translations/en.json @@ -4,6 +4,7 @@ "already_configured": "Device is already configured" }, "error": { + "cannot_connect": "Failed to connect", "connection_exception": "Failed to connect", "invalid_auth": "Invalid authentication", "unauthenticated_exception": "Invalid authentication", diff --git a/homeassistant/components/huisbaasje/translations/es-419.json b/homeassistant/components/huisbaasje/translations/es-419.json new file mode 100644 index 00000000000..c456531ee49 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "No se logr\u00f3 una conecci\u00f3n" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/et.json b/homeassistant/components/huisbaasje/translations/et.json index d079bf2a0c7..b4170142778 100644 --- a/homeassistant/components/huisbaasje/translations/et.json +++ b/homeassistant/components/huisbaasje/translations/et.json @@ -4,6 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { + "cannot_connect": "\u00dchendamine nurjus", "connection_exception": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", "unauthenticated_exception": "Vigane autentimine", diff --git a/homeassistant/components/huisbaasje/translations/hu.json b/homeassistant/components/huisbaasje/translations/hu.json new file mode 100644 index 00000000000..9d94d9d76ab --- /dev/null +++ b/homeassistant/components/huisbaasje/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", + "connection_exception": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unauthenticated_exception": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/id.json b/homeassistant/components/huisbaasje/translations/id.json new file mode 100644 index 00000000000..76e8805524e --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "connection_exception": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unauthenticated_exception": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/it.json b/homeassistant/components/huisbaasje/translations/it.json index 0171bdcd9f2..a8f899f9f82 100644 --- a/homeassistant/components/huisbaasje/translations/it.json +++ b/homeassistant/components/huisbaasje/translations/it.json @@ -4,6 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { + "cannot_connect": "Impossibile connettersi", "connection_exception": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unauthenticated_exception": "Autenticazione non valida", diff --git a/homeassistant/components/huisbaasje/translations/ko.json b/homeassistant/components/huisbaasje/translations/ko.json index bd25569d7c7..19387dfe542 100644 --- a/homeassistant/components/huisbaasje/translations/ko.json +++ b/homeassistant/components/huisbaasje/translations/ko.json @@ -4,6 +4,7 @@ "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", "connection_exception": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unauthenticated_exception": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/huisbaasje/translations/nl.json b/homeassistant/components/huisbaasje/translations/nl.json index 8cb09793af8..a13c1837b9f 100644 --- a/homeassistant/components/huisbaasje/translations/nl.json +++ b/homeassistant/components/huisbaasje/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "connection_exception": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unauthenticated_exception": "Ongeldige authenticatie", diff --git a/homeassistant/components/huisbaasje/translations/no.json b/homeassistant/components/huisbaasje/translations/no.json index 81351599c16..3eebfb72df2 100644 --- a/homeassistant/components/huisbaasje/translations/no.json +++ b/homeassistant/components/huisbaasje/translations/no.json @@ -4,6 +4,7 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { + "cannot_connect": "Tilkobling mislyktes", "connection_exception": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unauthenticated_exception": "Ugyldig godkjenning", diff --git a/homeassistant/components/huisbaasje/translations/pl.json b/homeassistant/components/huisbaasje/translations/pl.json index ab38d61de0b..c87d3d0be7d 100644 --- a/homeassistant/components/huisbaasje/translations/pl.json +++ b/homeassistant/components/huisbaasje/translations/pl.json @@ -4,6 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_exception": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unauthenticated_exception": "Niepoprawne uwierzytelnienie", diff --git a/homeassistant/components/huisbaasje/translations/pt.json b/homeassistant/components/huisbaasje/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/ru.json b/homeassistant/components/huisbaasje/translations/ru.json index a598320115d..c9fbe5cdcb2 100644 --- a/homeassistant/components/huisbaasje/translations/ru.json +++ b/homeassistant/components/huisbaasje/translations/ru.json @@ -4,6 +4,7 @@ "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.", "connection_exception": "\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.", "unauthenticated_exception": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", @@ -13,7 +14,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json index bb120ab60dd..b1e95586376 100644 --- a/homeassistant/components/huisbaasje/translations/zh-Hant.json +++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json @@ -4,6 +4,7 @@ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_exception": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unauthenticated_exception": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 1763e169d50..9500b74aba6 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -1,7 +1,9 @@ """Provides functionality to interact with humidifier devices.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Dict, List, Optional +from typing import Any, final import voluptuous as vol @@ -13,6 +15,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, @@ -20,7 +23,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 from .const import ( @@ -57,7 +60,7 @@ def is_on(hass, entity_id): 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: """Set up humidifier devices.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -86,21 +89,21 @@ 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) -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) class HumidifierEntity(ToggleEntity): - """Representation of a humidifier device.""" + """Base class for humidifier entities.""" @property - def capability_attributes(self) -> Dict[str, Any]: + def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" supported_features = self.supported_features or 0 data = { @@ -113,8 +116,9 @@ class HumidifierEntity(ToggleEntity): return data + @final @property - def state_attributes(self) -> Dict[str, Any]: + def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" supported_features = self.supported_features or 0 data = {} @@ -128,12 +132,12 @@ class HumidifierEntity(ToggleEntity): return data @property - def target_humidity(self) -> Optional[int]: + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return None @property - def mode(self) -> Optional[str]: + def mode(self) -> str | None: """Return the current mode, e.g., home, auto, baby. Requires SUPPORT_MODES. @@ -141,7 +145,7 @@ class HumidifierEntity(ToggleEntity): raise NotImplementedError @property - def available_modes(self) -> Optional[List[str]]: + def available_modes(self) -> list[str] | None: """Return a list of available modes. Requires SUPPORT_MODES. diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index 7e70c51df28..c2508770187 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -1,6 +1,4 @@ """Provides the constants needed for component.""" -from homeassistant.const import ATTR_MODE # noqa: F401 pylint: disable=unused-import - MODE_NORMAL = "normal" MODE_ECO = "eco" MODE_AWAY = "away" diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index 6bccd375207..fa9c1eb71e7 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -1,11 +1,12 @@ """Provides device actions for Humidifier.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_SUPPORTED_FEATURES, CONF_DEVICE_ID, CONF_DOMAIN, @@ -30,7 +31,7 @@ SET_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): "set_mode", vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), - vol.Required(const.ATTR_MODE): cv.string, + vol.Required(ATTR_MODE): cv.string, } ) @@ -39,7 +40,7 @@ ONOFF_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DO ACTION_SCHEMA = vol.Any(SET_HUMIDITY_SCHEMA, SET_MODE_SCHEMA, ONOFF_SCHEMA) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Humidifier devices.""" registry = await entity_registry.async_get_registry(hass) actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) @@ -78,11 +79,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if config[CONF_TYPE] == "set_humidity": @@ -90,7 +89,7 @@ async def async_call_action_from_config( service_data[const.ATTR_HUMIDITY] = config[const.ATTR_HUMIDITY] elif config[CONF_TYPE] == "set_mode": service = const.SERVICE_SET_MODE - service_data[const.ATTR_MODE] = config[const.ATTR_MODE] + service_data[ATTR_MODE] = config[ATTR_MODE] else: return await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN @@ -115,7 +114,7 @@ async def async_get_action_capabilities(hass, config): available_modes = state.attributes.get(const.ATTR_AVAILABLE_MODES, []) else: available_modes = [] - fields[vol.Required(const.ATTR_MODE)] = vol.In(available_modes) + fields[vol.Required(ATTR_MODE)] = vol.In(available_modes) else: return {} diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index 714a51ab016..02a667f2f68 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -1,11 +1,12 @@ """Provide the device automations for Humidifier.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_SUPPORTED_FEATURES, CONF_CONDITION, CONF_DEVICE_ID, @@ -28,7 +29,7 @@ MODE_CONDITION = DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): "is_mode", - vol.Required(const.ATTR_MODE): str, + vol.Required(ATTR_MODE): str, } ) @@ -37,7 +38,7 @@ CONDITION_SCHEMA = vol.Any(TOGGLE_CONDITION, MODE_CONDITION) async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Humidifier devices.""" registry = await entity_registry.async_get_registry(hass) conditions = await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) @@ -72,7 +73,7 @@ def async_condition_from_config( config = CONDITION_SCHEMA(config) if config[CONF_TYPE] == "is_mode": - attribute = const.ATTR_MODE + attribute = ATTR_MODE else: return toggle_entity.async_condition_from_config(config) @@ -97,7 +98,7 @@ async def async_get_condition_capabilities(hass, config): else: modes = [] - fields[vol.Required(const.ATTR_AVAILABLE_MODES)] = vol.In(modes) + fields[vol.Required(ATTR_MODE)] = vol.In(modes) return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index 6bc9682f79a..d0f462f6b0f 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Climate.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -48,7 +48,7 @@ TOGGLE_TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( TRIGGER_SCHEMA = vol.Any(TARGET_TRIGGER_SCHEMA, TOGGLE_TRIGGER_SCHEMA) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Humidifier devices.""" registry = await entity_registry.async_get_registry(hass) triggers = await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/humidifier/group.py b/homeassistant/components/humidifier/group.py index 1636054663d..234883ffd5a 100644 --- a/homeassistant/components/humidifier/group.py +++ b/homeassistant/components/humidifier/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/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index fafbb0a494a..d9ecafbc537 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -1,7 +1,7 @@ """Intents for the humidifier integration.""" import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv @@ -9,7 +9,6 @@ import homeassistant.helpers.config_validation as cv from . import ( ATTR_AVAILABLE_MODES, ATTR_HUMIDITY, - ATTR_MODE, DOMAIN, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, diff --git a/homeassistant/components/humidifier/reproduce_state.py b/homeassistant/components/humidifier/reproduce_state.py index e9b1777d63f..3f73ebf4e0a 100644 --- a/homeassistant/components/humidifier/reproduce_state.py +++ b/homeassistant/components/humidifier/reproduce_state.py @@ -1,29 +1,30 @@ """Module that groups code required to handle state restore for component.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable -from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType - -from .const import ( - ATTR_HUMIDITY, +from homeassistant.const import ( ATTR_MODE, - DOMAIN, - SERVICE_SET_HUMIDITY, - SERVICE_SET_MODE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) +from homeassistant.core import Context, HomeAssistant, State + +from .const import ATTR_HUMIDITY, DOMAIN, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE _LOGGER = logging.getLogger(__name__) async def _async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" cur_state = hass.states.get(state.entity_id) @@ -79,11 +80,11 @@ async def _async_reproduce_states( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" await asyncio.gather( diff --git a/homeassistant/components/humidifier/translations/de.json b/homeassistant/components/humidifier/translations/de.json index 24d8b01353e..a9bd89055ef 100644 --- a/homeassistant/components/humidifier/translations/de.json +++ b/homeassistant/components/humidifier/translations/de.json @@ -1,12 +1,19 @@ { "device_automation": { "action_type": { + "set_humidity": "Luftfeuchtigkeit f\u00fcr {entity_name} einstellen", "set_mode": "Wechsele Modus auf {entity_name}", "toggle": "{entity_name} umschalten", "turn_off": "Schalte {entity_name} aus", "turn_on": "Schalte {entity_name} an" }, + "condition_type": { + "is_mode": "{entity_name} ist auf einen bestimmten Modus festgelegt", + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, "trigger_type": { + "target_humidity_changed": "{entity_name} Soll-Luftfeuchtigkeit ge\u00e4ndert", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" } diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json new file mode 100644 index 00000000000..7dd723df738 --- /dev/null +++ b/homeassistant/components/humidifier/translations/hu.json @@ -0,0 +1,28 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "{entity_name} p\u00e1ratartalom be\u00e1ll\u00edt\u00e1sa", + "set_mode": "{entity_name} m\u00f3d m\u00f3dos\u00edt\u00e1sa", + "toggle": "{entity_name} be/kikapcsol\u00e1sa", + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_mode": "A(z) {entity_name} egy adott m\u00f3dra van \u00e1ll\u00edtva", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva" + }, + "trigger_type": { + "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + }, + "state": { + "_": { + "off": "Ki", + "on": "Be" + } + }, + "title": "P\u00e1r\u00e1s\u00edt\u00f3" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/id.json b/homeassistant/components/humidifier/translations/id.json new file mode 100644 index 00000000000..b06b2bfee45 --- /dev/null +++ b/homeassistant/components/humidifier/translations/id.json @@ -0,0 +1,28 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Setel kelembaban untuk {entity_name}", + "set_mode": "Ubah mode di {entity_name}", + "toggle": "Nyala/matikan {entity_name}", + "turn_off": "Matikan {entity_name}", + "turn_on": "Nyalakan {entity_name}" + }, + "condition_type": { + "is_mode": "{entity_name} disetel ke mode tertentu", + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala" + }, + "trigger_type": { + "target_humidity_changed": "Kelembapan target {entity_name} berubah", + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan" + } + }, + "state": { + "_": { + "off": "Mati", + "on": "Nyala" + } + }, + "title": "Pelembab" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/ko.json b/homeassistant/components/humidifier/translations/ko.json index c484a532156..9c556f246e3 100644 --- a/homeassistant/components/humidifier/translations/ko.json +++ b/homeassistant/components/humidifier/translations/ko.json @@ -1,21 +1,21 @@ { "device_automation": { "action_type": { - "set_humidity": "{entity_name} \uc2b5\ub3c4 \uc124\uc815\ud558\uae30", - "set_mode": "{entity_name} \uc758 \uc6b4\uc804 \ubaa8\ub4dc \ubcc0\uacbd", - "toggle": "{entity_name} \ud1a0\uae00", - "turn_off": "{entity_name} \ub044\uae30", - "turn_on": "{entity_name} \ucf1c\uae30" + "set_humidity": "{entity_name}\uc758 \uc2b5\ub3c4 \uc124\uc815\ud558\uae30", + "set_mode": "{entity_name}\uc758 \uc6b4\uc804 \ubaa8\ub4dc \ubcc0\uacbd", + "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30", + "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", + "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" }, "condition_type": { - "is_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + "is_mode": "{entity_name}\uc774(\uac00) \ud2b9\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "target_humidity_changed": "{entity_name} \ubaa9\ud45c \uc2b5\ub3c4\uac00 \ubcc0\uacbd\ub420 \ub54c", - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + "target_humidity_changed": "{entity_name}\uc758 \ubaa9\ud45c \uc2b5\ub3c4\uac00 \ubcc0\uacbd\ub418\uc5c8\uc744 \ub54c", + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/humidifier/translations/nl.json b/homeassistant/components/humidifier/translations/nl.json index 311943bbd23..9505a6a0838 100644 --- a/homeassistant/components/humidifier/translations/nl.json +++ b/homeassistant/components/humidifier/translations/nl.json @@ -1,9 +1,17 @@ { "device_automation": { "action_type": { + "set_humidity": "Luchtvochtigheid instellen voor {entity_name}", + "set_mode": "Wijzig modus van {entity_name}", + "toggle": "Schakel {entity_name}", "turn_off": "{entity_name} uitschakelen", "turn_on": "{entity_name} inschakelen" }, + "condition_type": { + "is_mode": "{entity_name} is ingesteld op een specifieke modus", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} staat aan" + }, "trigger_type": { "target_humidity_changed": "{entity_name} doel luchtvochtigheid gewijzigd", "turned_off": "{entity_name} is uitgeschakeld", @@ -15,5 +23,6 @@ "off": "Uit", "on": "Aan" } - } + }, + "title": "Luchtbevochtiger" } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/zh-Hans.json b/homeassistant/components/humidifier/translations/zh-Hans.json index 8fa6b0be0da..d21c7bf61f7 100644 --- a/homeassistant/components/humidifier/translations/zh-Hans.json +++ b/homeassistant/components/humidifier/translations/zh-Hans.json @@ -8,9 +8,12 @@ "turn_on": "\u6253\u5f00 {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u5df2\u5173\u95ed" + "is_mode": "{entity_name} \u5904\u4e8e\u6307\u5b9a\u6a21\u5f0f", + "is_off": "{entity_name} \u5df2\u5173\u95ed", + "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { + "target_humidity_changed": "{entity_name} \u7684\u8bbe\u5b9a\u6e7f\u5ea6\u53d8\u5316", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 7555146ba8e..2a5c5061cae 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -132,9 +132,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DEVICE_INFO: device_info, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -180,8 +180,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 34ae94e4b88..928c4b4819f 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -10,8 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import async_get_device_info -from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, HUB_EXCEPTIONS -from .const import DOMAIN # pylint:disable=unused-import +from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, DOMAIN, HUB_EXCEPTIONS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index e90b315fd16..58c7e90994c 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -1,5 +1,6 @@ """Support for hunter douglas shades.""" import asyncio +from contextlib import suppress import logging from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA @@ -65,12 +66,10 @@ async def async_setup_entry(hass, entry, async_add_entities): # possible shade = PvShade(raw_shade, pv_request) name_before_refresh = shade.name - try: + with suppress(asyncio.TimeoutError): async with async_timeout.timeout(1): await shade.refresh() - except asyncio.TimeoutError: - # Forced refresh is not required for setup - pass + if ATTR_POSITION_DATA not in shade.raw_data: _LOGGER.info( "The %s shade was skipped because it is missing position data", @@ -111,7 +110,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): self._current_cover_position = MIN_POSITION @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name} diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 33c7e7129fc..c30cde8d043 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -71,7 +71,7 @@ class PowerViewScene(HDEntity, Scene): return self._scene.name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name} diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 130e8dd507a..d66671fe1ea 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -1,6 +1,7 @@ """Support for hunterdouglass_powerview sensors.""" from aiopvapi.resources.shade import factory as PvShade +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback @@ -45,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class PowerViewShadeBatterySensor(ShadeEntity): +class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): """Representation of an shade battery charge sensor.""" @property diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json index 61461d1796c..063e0dad3c4 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -4,8 +4,8 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", - "unknown": "V\u00e1ratlan hiba" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/id.json b/homeassistant/components/hunterdouglas_powerview/translations/id.json new file mode 100644 index 00000000000..2d21f87bf67 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "link": { + "description": "Ingin menyiapkan {name} ({host})?", + "title": "Hubungkan ke PowerView Hub" + }, + "user": { + "data": { + "host": "Alamat IP" + }, + "title": "Hubungkan ke PowerView Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index e003b25ea85..c90e5cb6d9c 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -32,9 +32,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -45,8 +45,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index 7d19fcc8fdf..45ac0e45ad9 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass, entry, async_add_entities): raise UpdateFailed(f"Authentication failed: {err}") from err except ClientConnectorError as err: raise UpdateFailed(f"Network not available: {err}") from err - except Exception as err: # pylint: disable=broad-except + except Exception as err: raise UpdateFailed(f"Error occurred while fetching data: {err}") from err coordinator = DataUpdateCoordinator( @@ -174,7 +174,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): return DEVICE_CLASS_PROBLEM @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not ( self.coordinator.last_update_success diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py index 1a49bffd2f5..556505101eb 100644 --- a/homeassistant/components/hvv_departures/config_flow.py +++ b/homeassistant/components/hvv_departures/config_flow.py @@ -11,12 +11,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from .const import ( # pylint:disable=unused-import - CONF_FILTER, - CONF_REAL_TIME, - CONF_STATION, - DOMAIN, -) +from .const import CONF_FILTER, CONF_REAL_TIME, CONF_STATION, DOMAIN from .hub import GTIHub _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index d6957e6beec..35fc137a0f0 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -6,9 +6,9 @@ from aiohttp import ClientConnectorError from pygti.exceptions import InvalidAuth from pytz import timezone +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import utcnow @@ -43,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices): async_add_devices([sensor], True) -class HVVDepartureSensor(Entity): +class HVVDepartureSensor(SensorEntity): """HVVDepartureSensor class.""" def __init__(self, hass, config_entry, session, hub): @@ -199,6 +199,6 @@ class HVVDepartureSensor(Entity): return DEVICE_CLASS_TIMESTAMP @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self.attr diff --git a/homeassistant/components/hvv_departures/translations/he.json b/homeassistant/components/hvv_departures/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/hu.json b/homeassistant/components/hvv_departures/translations/hu.json new file mode 100644 index 00000000000..91da2d13a7c --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/hu.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/id.json b/homeassistant/components/hvv_departures/translations/id.json new file mode 100644 index 00000000000..d43b306d292 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/id.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "no_results": "Tidak ada hasil. Coba stasiun/alamat lainnya" + }, + "step": { + "station": { + "data": { + "station": "Stasiun/Alamat" + }, + "title": "Masukkan Stasiun/Alamat" + }, + "station_select": { + "data": { + "station": "Stasiun/Alamat" + }, + "title": "Pilih Stasiun/Alamat" + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke API HVV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "Pilih jalur", + "offset": "Tenggang (menit)", + "real_time": "Gunakan data waktu nyata" + }, + "description": "Ubah opsi untuk sensor keberangkatan ini", + "title": "Opsi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/ko.json b/homeassistant/components/hvv_departures/translations/ko.json index ea6ef8bc23a..ca8de2ff5cd 100644 --- a/homeassistant/components/hvv_departures/translations/ko.json +++ b/homeassistant/components/hvv_departures/translations/ko.json @@ -13,7 +13,7 @@ "data": { "station": "\uc2a4\ud14c\uc774\uc158 / \uc8fc\uc18c" }, - "title": "\uc2a4\ud14c\uc774\uc158 / \uc8fc\uc18c \uc785\ub825\ud558\uae30" + "title": "\uc2a4\ud14c\uc774\uc158 / \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" }, "station_select": { "data": { @@ -27,7 +27,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "HVV API \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "HVV API\uc5d0 \uc5f0\uacb0\ud558\uae30" } } }, diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json index 8c80ae5b942..09c8b5b60e7 100644 --- a/homeassistant/components/hvv_departures/translations/nl.json +++ b/homeassistant/components/hvv_departures/translations/nl.json @@ -5,9 +5,22 @@ }, "error": { "cannot_connect": "Kon niet verbinden", - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "no_results": "Geen resultaten. Probeer het met een ander station/adres" }, "step": { + "station": { + "data": { + "station": "Station/Adres" + }, + "title": "Voer station/adres in" + }, + "station_select": { + "data": { + "station": "Station/Adres" + }, + "title": "Selecteer Station/Adres" + }, "user": { "data": { "host": "Host", @@ -17,5 +30,18 @@ "title": "Maak verbinding met de HVV API" } } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "Selecteer lijnen", + "offset": "Offset (minuten)", + "real_time": "Gebruik realtime gegevens" + }, + "description": "Wijzig opties voor deze vertreksensor", + "title": "Opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/ru.json b/homeassistant/components/hvv_departures/translations/ru.json index 6ae27715033..55e60b23c32 100644 --- a/homeassistant/components/hvv_departures/translations/ru.json +++ b/homeassistant/components/hvv_departures/translations/ru.json @@ -25,7 +25,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 API HVV" } diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 08827baae68..1f1b2c03157 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -144,14 +144,7 @@ class HydrawiseEntity(Entity): self.async_schedule_update_ha_state(True) @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return DEVICE_MAP[self._sensor_type][ - DEVICE_MAP_INDEX.index("UNIT_OF_MEASURE_INDEX") - ] - - @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION, "identifier": self.data.get("relay")} diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 6a0c6ab0d80..62108afbded 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -3,12 +3,12 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.util import dt -from . import DATA_HYDRAWISE, SENSORS, HydrawiseEntity +from . import DATA_HYDRAWISE, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS, HydrawiseEntity _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class HydrawiseSensor(HydrawiseEntity): +class HydrawiseSensor(HydrawiseEntity, SensorEntity): """A sensor implementation for Hydrawise device.""" @property @@ -44,6 +44,13 @@ class HydrawiseSensor(HydrawiseEntity): """Return the state of the sensor.""" return self._state + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index("UNIT_OF_MEASURE_INDEX") + ] + def update(self): """Get the latest data and updates the states.""" mydata = self.hass.data[DATA_HYDRAWISE].data diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 9e35ae2e6b8..03b892ce83b 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -1,8 +1,10 @@ """The Hyperion component.""" +from __future__ import annotations import asyncio +from contextlib import suppress import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast +from typing import Any, Callable, cast from awesomeversion import AwesomeVersion from hyperion import client, const as hyperion_const @@ -70,7 +72,7 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str: return f"{server_id}_{instance}_{name}" -def split_hyperion_unique_id(unique_id: str) -> Optional[Tuple[str, int, str]]: +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)) if len(data) != 3: @@ -92,7 +94,7 @@ def create_hyperion_client( async def async_create_connect_hyperion_client( *args: Any, **kwargs: Any, -) -> Optional[client.HyperionClient]: +) -> client.HyperionClient | None: """Create and connect a Hyperion Client.""" hyperion_client = create_hyperion_client(*args, **kwargs) @@ -158,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady version = await hyperion_client.async_sysinfo_version() if version is not None: - try: + with suppress(ValueError): if AwesomeVersion(version) < AwesomeVersion(HYPERION_VERSION_WARN_CUTOFF): _LOGGER.warning( "Using a Hyperion server version < %s is not recommended -- " @@ -167,8 +169,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b HYPERION_VERSION_WARN_CUTOFF, HYPERION_RELEASES_URL, ) - except ValueError: - pass # Client needs authentication, but no token provided? => Reauth. auth_resp = await hyperion_client.async_is_auth_required() @@ -207,17 +207,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b CONF_ON_UNLOAD: [], } - async def async_instances_to_clients(response: Dict[str, Any]) -> None: + async def async_instances_to_clients(response: dict[str, Any]) -> None: """Convert instances to Hyperion clients.""" if not response or hyperion_const.KEY_DATA not in response: return await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA]) - async def async_instances_to_clients_raw(instances: List[Dict[str, Any]]) -> None: + async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None: """Convert instances to Hyperion clients.""" registry = await async_get_registry(hass) - running_instances: Set[int] = set() - stopped_instances: Set[int] = set() + running_instances: set[int] = set() + stopped_instances: set[int] = set() existing_instances = hass.data[DOMAIN][config_entry.entry_id][ CONF_INSTANCE_CLIENTS ] @@ -281,12 +281,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def setup_then_listen() -> None: await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_setup(config_entry, platform) + for platform in PLATFORMS ] ) assert hyperion_client - await async_instances_to_clients_raw(hyperion_client.instances) + if hyperion_client.instances is not None: + await async_instances_to_clients_raw(hyperion_client.instances) hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( config_entry.add_update_listener(_async_entry_updated) ) @@ -309,8 +310,8 @@ async def async_unload_entry( unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 642bc0e93fd..7ceedcbf005 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from contextlib import suppress import logging -from typing import Any, Dict, Optional +from typing import Any from urllib.parse import urlparse from hyperion import client, const @@ -26,14 +27,15 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from . import create_hyperion_client - -# pylint: disable=unused-import from .const import ( CONF_AUTH_ID, CONF_CREATE_TOKEN, + CONF_EFFECT_HIDE_LIST, + CONF_EFFECT_SHOW_LIST, CONF_PRIORITY, DEFAULT_ORIGIN, DEFAULT_PRIORITY, @@ -111,9 +113,9 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Instantiate config flow.""" - self._data: Dict[str, Any] = {} - self._request_token_task: Optional[asyncio.Task] = None - self._auth_id: Optional[str] = None + self._data: dict[str, Any] = {} + self._request_token_task: asyncio.Task | None = None + self._auth_id: str | None = None self._require_confirm: bool = False self._port_ui: int = const.DEFAULT_PORT_UI @@ -128,7 +130,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def _advance_to_auth_step_if_necessary( self, hyperion_client: client.HyperionClient - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Determine if auth is required.""" auth_resp = await hyperion_client.async_is_auth_required() @@ -143,7 +145,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, config_data: ConfigType, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle a reauthentication flow.""" self._data = dict(config_data) async with self._create_client(raw_connection=True) as hyperion_client: @@ -152,8 +154,8 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): 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]: + self, discovery_info: dict[str, Any] + ) -> dict[str, Any]: """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', @@ -221,11 +223,10 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - # pylint: disable=arguments-differ async def async_step_user( self, - user_input: Optional[ConfigType] = None, - ) -> Dict[str, Any]: + user_input: ConfigType | None = None, + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" errors = {} if user_input: @@ -255,15 +256,13 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): if not self._request_token_task.done(): self._request_token_task.cancel() - try: + with suppress(asyncio.CancelledError): await self._request_token_task - except asyncio.CancelledError: - pass self._request_token_task = None async def _request_token_task_func(self, auth_id: str) -> None: """Send an async_request_token request.""" - auth_resp: Optional[Dict[str, Any]] = None + auth_resp: dict[str, Any] | None = None async with self._create_client(raw_connection=True) as hyperion_client: if hyperion_client: # The Hyperion-py client has a default timeout of 3 minutes on this request. @@ -284,7 +283,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): # used to open a URL, that the user already knows the address of). return f"http://{self._data[CONF_HOST]}:{self._port_ui}" - async def _can_login(self) -> Optional[bool]: + async def _can_login(self) -> bool | None: """Verify login details.""" async with self._create_client(raw_connection=True) as hyperion_client: if not hyperion_client: @@ -297,8 +296,8 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, - user_input: Optional[ConfigType] = None, - ) -> Dict[str, Any]: + user_input: ConfigType | None = None, + ) -> dict[str, Any]: """Handle the auth step of a flow.""" errors = {} if user_input: @@ -326,8 +325,8 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_create_token( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Send a request for a new token.""" if user_input is None: self._auth_id = client.generate_random_auth_id() @@ -352,8 +351,8 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_create_token_external( - self, auth_resp: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, auth_resp: ConfigType | None = None + ) -> dict[str, Any]: """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,8 +364,8 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_external_step_done(next_step_id="create_token_fail") async def async_step_create_token_success( - self, _: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, _: ConfigType | None = None + ) -> dict[str, Any]: """Create an entry after successful token creation.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -381,16 +380,16 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() async def async_step_create_token_fail( - self, _: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, _: ConfigType | None = None + ) -> dict[str, Any]: """Show an error on the auth form.""" # Clean-up the request task. await self._cancel_request_token_task() return self.async_abort(reason="auth_new_token_not_granted_error") async def async_step_confirm( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: return self.async_show_form( @@ -440,13 +439,44 @@ class HyperionOptionsFlow(OptionsFlow): """Initialize a Hyperion options flow.""" self._config_entry = config_entry + def _create_client(self) -> client.HyperionClient: + """Create and connect a client instance.""" + return create_hyperion_client( + self._config_entry.data[CONF_HOST], + self._config_entry.data[CONF_PORT], + token=self._config_entry.data.get(CONF_TOKEN), + ) + async def async_step_init( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Manage the options.""" + + effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES} + async with self._create_client() as hyperion_client: + if not hyperion_client: + return self.async_abort(reason="cannot_connect") + for effect in hyperion_client.effects or []: + if const.KEY_NAME in effect: + effects[effect[const.KEY_NAME]] = effect[const.KEY_NAME] + + # If a new effect is added to Hyperion, we always want it to show by default. So + # rather than store a 'show list' in the config entry, we store a 'hide list'. + # However, it's more intuitive to ask the user to select which effects to show, + # so we inverse the meaning prior to storage. + if user_input is not None: + effect_show_list = user_input.pop(CONF_EFFECT_SHOW_LIST) + user_input[CONF_EFFECT_HIDE_LIST] = sorted( + set(effects) - set(effect_show_list) + ) return self.async_create_entry(title="", data=user_input) + default_effect_show_list = list( + set(effects) + - set(self._config_entry.options.get(CONF_EFFECT_HIDE_LIST, [])) + ) + return self.async_show_form( step_id="init", data_schema=vol.Schema( @@ -457,6 +487,10 @@ class HyperionOptionsFlow(OptionsFlow): CONF_PRIORITY, DEFAULT_PRIORITY ), ): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), + vol.Optional( + CONF_EFFECT_SHOW_LIST, + default=default_effect_show_list, + ): cv.multi_select(effects), } ), ) diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 64c2f20052b..994ef580c91 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -31,6 +31,8 @@ CONF_INSTANCE_CLIENTS = "INSTANCE_CLIENTS" CONF_ON_UNLOAD = "ON_UNLOAD" CONF_PRIORITY = "priority" CONF_ROOT_CLIENT = "ROOT_CLIENT" +CONF_EFFECT_HIDE_LIST = "effect_hide_list" +CONF_EFFECT_SHOW_LIST = "effect_show_list" DEFAULT_NAME = "Hyperion" DEFAULT_ORIGIN = "Home Assistant" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 7bb8a75dfc7..248a45ec753 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools import logging from types import MappingProxyType -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple +from typing import Any, Callable, Mapping, Sequence from hyperion import client, const @@ -28,6 +28,7 @@ import homeassistant.util.color as color_util from . import get_hyperion_unique_id, listen_for_instance_updates from .const import ( + CONF_EFFECT_HIDE_LIST, CONF_INSTANCE_CLIENTS, CONF_PRIORITY, DEFAULT_ORIGIN, @@ -63,7 +64,7 @@ DEFAULT_EFFECT = KEY_EFFECT_SOLID DEFAULT_NAME = "Hyperion" DEFAULT_PORT = const.DEFAULT_PORT_JSON DEFAULT_HDMI_PRIORITY = 880 -DEFAULT_EFFECT_LIST: List[str] = [] +DEFAULT_EFFECT_LIST: list[str] = [] SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT @@ -142,12 +143,12 @@ class HyperionBaseLight(LightEntity): self._rgb_color: Sequence[int] = DEFAULT_COLOR self._effect: str = KEY_EFFECT_SOLID - self._static_effect_list: List[str] = [KEY_EFFECT_SOLID] + 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._effect_list: List[str] = self._static_effect_list[:] + self._effect_list: list[str] = self._static_effect_list[:] - self._client_callbacks = { + self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = { f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment, f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components, f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list, @@ -176,7 +177,7 @@ class HyperionBaseLight(LightEntity): return self._brightness @property - def hs_color(self) -> Tuple[float, float]: + def hs_color(self) -> tuple[float, float]: """Return last color value set.""" return color_util.color_RGB_to_hs(*self._rgb_color) @@ -196,7 +197,7 @@ class HyperionBaseLight(LightEntity): return self._effect @property - def effect_list(self) -> List[str]: + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._effect_list @@ -217,7 +218,10 @@ class HyperionBaseLight(LightEntity): def _get_option(self, key: str) -> Any: """Get a value from the provided options.""" - defaults = {CONF_PRIORITY: DEFAULT_PRIORITY} + defaults = { + CONF_PRIORITY: DEFAULT_PRIORITY, + CONF_EFFECT_HIDE_LIST: [], + } return self._options.get(key, defaults[key]) async def async_turn_on(self, **kwargs: Any) -> None: @@ -236,9 +240,10 @@ class HyperionBaseLight(LightEntity): # == Set brightness == if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] - for item in self._client.adjustment: - if const.KEY_ID in item: - if not await self._client.async_send_set_adjustment( + for item in self._client.adjustment or []: + if ( + const.KEY_ID in item + and not await self._client.async_send_set_adjustment( **{ const.KEY_ADJUSTMENT: { const.KEY_BRIGHTNESS: int( @@ -247,8 +252,9 @@ class HyperionBaseLight(LightEntity): const.KEY_ID: item[const.KEY_ID], } } - ): - return + ) + ): + return # == Set an external source if ( @@ -305,9 +311,9 @@ class HyperionBaseLight(LightEntity): def _set_internal_state( self, - brightness: Optional[int] = None, - rgb_color: Optional[Sequence[int]] = None, - effect: Optional[str] = None, + brightness: int | None = None, + rgb_color: Sequence[int] | None = None, + effect: str | None = None, ) -> None: """Set the internal state.""" if brightness is not None: @@ -318,12 +324,12 @@ class HyperionBaseLight(LightEntity): self._effect = effect @callback - def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None: + def _update_components(self, _: dict[str, Any] | None = None) -> None: """Update Hyperion components.""" self.async_write_ha_state() @callback - def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None: + def _update_adjustment(self, _: dict[str, Any] | None = None) -> None: """Update Hyperion adjustments.""" if self._client.adjustment: brightness_pct = self._client.adjustment[0].get( @@ -337,7 +343,7 @@ class HyperionBaseLight(LightEntity): self.async_write_ha_state() @callback - def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None: + def _update_priorities(self, _: dict[str, Any] | None = None) -> None: """Update Hyperion priorities.""" priority = self._get_priority_entry_that_dictates_state() if priority and self._allow_priority_update(priority): @@ -361,17 +367,23 @@ class HyperionBaseLight(LightEntity): self.async_write_ha_state() @callback - def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None: + def _update_effect_list(self, _: dict[str, Any] | None = None) -> None: """Update Hyperion effects.""" if not self._client.effects: return - effect_list: List[str] = [] + effect_list: list[str] = [] + hide_effects = self._get_option(CONF_EFFECT_HIDE_LIST) + for effect in self._client.effects or []: if const.KEY_NAME in effect: - effect_list.append(effect[const.KEY_NAME]) - if effect_list: - self._effect_list = self._static_effect_list + effect_list - self.async_write_ha_state() + effect_name = effect[const.KEY_NAME] + if effect_name not in hide_effects: + effect_list.append(effect_name) + + self._effect_list = [ + effect for effect in self._static_effect_list if effect not in hide_effects + ] + effect_list + self.async_write_ha_state() @callback def _update_full_state(self) -> None: @@ -391,13 +403,12 @@ class HyperionBaseLight(LightEntity): ) @callback - def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None: + def _update_client(self, _: dict[str, Any] | None = None) -> None: """Update client connection state.""" self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Register callbacks when entity added to hass.""" - assert self.hass self.async_on_remove( async_dispatcher_connect( self.hass, @@ -420,13 +431,18 @@ class HyperionBaseLight(LightEntity): """Whether or not to support setting external effects from the light entity.""" return True - def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]: + def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None: """Get the relevant Hyperion priority entry to consider.""" # Return the visible priority (whether or not it is the HA priority). - return self._client.visible_priority # type: ignore[no-any-return] + + # Explicit type specifier to ensure this works when the underlying (typed) + # library is installed along with the tests. Casts would trigger a + # redundant-cast warning in this case. + priority: dict[str, Any] | None = self._client.visible_priority + return priority # pylint: disable=no-self-use - def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool: + def _allow_priority_update(self, priority: dict[str, Any] | None = None) -> bool: """Determine whether to allow a priority to update internal state.""" return True @@ -521,7 +537,7 @@ class HyperionPriorityLight(HyperionBaseLight): """Whether or not to support setting external effects from the light entity.""" return False - def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]: + def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None: """Get the relevant Hyperion priority entry to consider.""" # Return the active priority (if any) at the configured HA priority. for candidate in self._client.priorities or []: @@ -530,11 +546,15 @@ class HyperionPriorityLight(HyperionBaseLight): if candidate[const.KEY_PRIORITY] == self._get_option( CONF_PRIORITY ) and candidate.get(const.KEY_ACTIVE, False): - return candidate # type: ignore[no-any-return] + # Explicit type specifier to ensure this works when the underlying + # (typed) library is installed along with the tests. Casts would trigger + # a redundant-cast warning in this case. + output: dict[str, Any] = candidate + return output return None @classmethod - def _is_priority_entry_black(cls, priority: Optional[Dict[str, Any]]) -> bool: + 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 @@ -544,8 +564,7 @@ class HyperionPriorityLight(HyperionBaseLight): return True return False - # pylint: disable=no-self-use - def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool: + def _allow_priority_update(self, priority: dict[str, Any] | None = None) -> bool: """Determine whether to allow a Hyperion priority to update entity attributes.""" # Black is treated as 'off' (and Home Assistant does not support selecting black # from the color selector). Do not set our internal attributes if the priority is diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json index ca7ed238f0b..54beb7704c9 100644 --- a/homeassistant/components/hyperion/strings.json +++ b/homeassistant/components/hyperion/strings.json @@ -45,9 +45,10 @@ "step": { "init": { "data": { - "priority": "Hyperion priority to use for colors and effects" + "priority": "Hyperion priority to use for colors and effects", + "effect_show_list": "Hyperion effects to show" } } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 9d90e1e12ef..4a4f8d4da13 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -1,7 +1,8 @@ """Switch platform for Hyperion.""" +from __future__ import annotations import functools -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable from hyperion import client from hyperion.const import ( @@ -157,7 +158,7 @@ class HyperionComponentSwitch(SwitchEntity): @property def is_on(self) -> bool: """Return true if the switch is on.""" - for component in self._client.components: + for component in self._client.components or []: if component[KEY_NAME] == self._component_name: return bool(component.setdefault(KEY_ENABLED, False)) return False @@ -178,24 +179,21 @@ class HyperionComponentSwitch(SwitchEntity): } ) - # pylint: disable=unused-argument async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self._async_send_set_component(True) - # pylint: disable=unused-argument async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self._async_send_set_component(False) @callback - def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None: + def _update_components(self, _: dict[str, Any] | None = None) -> None: """Update Hyperion components.""" self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Register callbacks when entity added to hass.""" - assert self.hass self.async_on_remove( async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/hyperion/translations/ca.json b/homeassistant/components/hyperion/translations/ca.json index 50ac384d8ca..3a1de53102b 100644 --- a/homeassistant/components/hyperion/translations/ca.json +++ b/homeassistant/components/hyperion/translations/ca.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Efectes d'Hyperion a mostrar", "priority": "Prioritat Hyperion a utilitzar per als colors i efectes" } } diff --git a/homeassistant/components/hyperion/translations/el.json b/homeassistant/components/hyperion/translations/el.json new file mode 100644 index 00000000000..5ba51046f21 --- /dev/null +++ b/homeassistant/components/hyperion/translations/el.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "\u0395\u03c6\u03ad Hyperion \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/en.json b/homeassistant/components/hyperion/translations/en.json index d1277b411e0..38ab5cf7055 100644 --- a/homeassistant/components/hyperion/translations/en.json +++ b/homeassistant/components/hyperion/translations/en.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Hyperion effects to show", "priority": "Hyperion priority to use for colors and effects" } } diff --git a/homeassistant/components/hyperion/translations/es-419.json b/homeassistant/components/hyperion/translations/es-419.json new file mode 100644 index 00000000000..ee8c8c108d9 --- /dev/null +++ b/homeassistant/components/hyperion/translations/es-419.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Efectos de Hyperion a mostrar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/et.json b/homeassistant/components/hyperion/translations/et.json index a225b7f2c47..dba661d24c2 100644 --- a/homeassistant/components/hyperion/translations/et.json +++ b/homeassistant/components/hyperion/translations/et.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Kuvatavad Hyperioni efektid", "priority": "V\u00e4rvide ja efektide puhul on kasutatavad Hyperioni eelistused" } } diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index f69fd6acdc6..57870c3b3ef 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Effets Hyperion \u00e0 montrer", "priority": "Priorit\u00e9 Hyperion \u00e0 utiliser pour les couleurs et les effets" } } diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index 50ccd9f3b63..cfe649d9d5e 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -1,13 +1,26 @@ { "config": { "abort": { - "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" }, "step": { "auth": { "data": { "create_token": "\u00daj token automatikus l\u00e9trehoz\u00e1sa" } + }, + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } } } } diff --git a/homeassistant/components/hyperion/translations/id.json b/homeassistant/components/hyperion/translations/id.json new file mode 100644 index 00000000000..c1c2a62e0d9 --- /dev/null +++ b/homeassistant/components/hyperion/translations/id.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "auth_new_token_not_granted_error": "Token yang baru dibuat tidak disetujui di antarmuka Hyperion", + "auth_new_token_not_work_error": "Gagal mengautentikasi menggunakan token yang baru dibuat", + "auth_required_error": "Gagal menentukan apakah otorisasi diperlukan", + "cannot_connect": "Gagal terhubung", + "no_id": "Instans Hyperion Ambilight tidak melaporkan ID-nya", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_access_token": "Token akses tidak valid" + }, + "step": { + "auth": { + "data": { + "create_token": "Buat token baru secara otomatis", + "token": "Atau berikan token yang sudah ada sebelumnya" + }, + "description": "Konfigurasikan otorisasi ke server Hyperion Ambilight Anda" + }, + "confirm": { + "description": "Apakah Anda ingin menambahkan Hyperion Ambilight ini ke Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Konfirmasikan penambahan layanan Hyperion Ambilight" + }, + "create_token": { + "description": "Pilih **Kirim** di bawah ini untuk meminta token autentikasi baru. Anda akan diarahkan ke antarmuka Hyperion untuk menyetujui permintaan. Pastikan ID yang ditampilkan adalah \"{auth_id}\"", + "title": "Buat token autentikasi baru secara otomatis" + }, + "create_token_external": { + "title": "Terima token baru di antarmuka Hyperion" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Prioritas hyperion digunakan untuk warna dan efek" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index b03b368d039..81f049125ed 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Effetti di Hyperion da mostrare", "priority": "Priorit\u00e0 Hyperion da usare per colori ed effetti" } } diff --git a/homeassistant/components/hyperion/translations/ko.json b/homeassistant/components/hyperion/translations/ko.json index 295d418da12..f42450675fb 100644 --- a/homeassistant/components/hyperion/translations/ko.json +++ b/homeassistant/components/hyperion/translations/ko.json @@ -3,7 +3,11 @@ "abort": { "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "auth_new_token_not_granted_error": "\uc0c8\ub85c \uc0dd\uc131\ub41c \ud1a0\ud070\uc774 Hyperion UI\uc5d0\uc11c \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4", + "auth_new_token_not_work_error": "\uc0c8\ub85c \uc0dd\uc131\ub41c \ud1a0\ud070\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc778\uc99d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "auth_required_error": "\uc778\uc99d\uc774 \ud544\uc694\ud55c\uc9c0 \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "no_id": "Hyperion Amblight \uc778\uc2a4\ud134\uc2a4\uac00 \ud574\ub2f9 ID\ub97c \ubcf4\uace0\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -11,6 +15,24 @@ "invalid_access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { + "auth": { + "data": { + "create_token": "\uc0c8\ub85c\uc6b4 \ud1a0\ud070\uc744 \uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\uae30", + "token": "\ub610\ub294 \uae30\uc874 \ud1a0\ud070 \uc81c\uacf5\ud558\uae30" + }, + "description": "Hyperion Amblight \uc11c\ubc84\uc5d0 \ub300\ud55c \uad8c\ud55c \uad6c\uc131\ud558\uae30" + }, + "confirm": { + "description": "\uc774 Hyperion Amblight\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n**\ud638\uc2a4\ud2b8**: {host}\n**\ud3ec\ud2b8**: {port}\n**ID**: {id}", + "title": "Hyperion Amblight \uc11c\ube44\uc2a4 \ucd94\uac00 \ud655\uc778\ud558\uae30" + }, + "create_token": { + "description": "\uc544\ub798 **\ud655\uc778**\uc744 \uc120\ud0dd\ud558\uc5ec \uc0c8\ub85c\uc6b4 \uc778\uc99d \ud1a0\ud070\uc744 \uc694\uccad\ud574\uc8fc\uc138\uc694. \uc694\uccad\uc744 \uc2b9\uc778\ud558\ub3c4\ub85d Hyperion UI\ub85c \ub9ac\ub514\ub809\uc158\ub429\ub2c8\ub2e4. \ud45c\uc2dc\ub41c ID\uac00 \"{auth_id}\"\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694", + "title": "\uc0c8\ub85c\uc6b4 \uc778\uc99d \ud1a0\ud070\uc744 \uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\uae30" + }, + "create_token_external": { + "title": "Hyperion UI\uc5d0\uc11c \uc0c8\ub85c\uc6b4 \ud1a0\ud070 \uc218\ub77d\ud558\uae30" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", @@ -18,5 +40,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "\ucd9c\ub825\ud560 Hyperion \ud6a8\uacfc", + "priority": "\uc0c9\uc0c1 \ubc0f \ud6a8\uacfc\uc5d0 \uc0ac\uc6a9\ud560 Hyperion \uc6b0\uc120 \uc21c\uc704" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json index 0898272e4a2..056971b435f 100644 --- a/homeassistant/components/hyperion/translations/nl.json +++ b/homeassistant/components/hyperion/translations/nl.json @@ -7,6 +7,7 @@ "auth_new_token_not_work_error": "Verificatie met nieuw aangemaakt token mislukt", "auth_required_error": "Kan niet bepalen of autorisatie vereist is", "cannot_connect": "Kan geen verbinding maken", + "no_id": "De Hyperion Ambilight instantie heeft zijn id niet gerapporteerd", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { @@ -16,8 +17,18 @@ "step": { "auth": { "data": { - "create_token": "Maak automatisch een nieuw token aan" - } + "create_token": "Maak automatisch een nieuw token aan", + "token": "Of geef een reeds bestaand token op" + }, + "description": "Configureer autorisatie voor uw Hyperion Ambilight-server" + }, + "confirm": { + "description": "Wilt u deze Hyperion Ambilight toevoegen aan Home Assistant? \n\n ** Host: ** {host}\n ** Poort: ** {port}\n ** ID **: {id}", + "title": "Bevestig de toevoeging van Hyperion Ambilight-service" + }, + "create_token": { + "description": "Kies **Submit** hieronder om een nieuw authenticatie token aan te vragen. U wordt doorgestuurd naar de Hyperion UI om de aanvraag goed te keuren. Controleer of de getoonde id \"{auth_id}\" is.", + "title": "Automatisch nieuw authenticatie token aanmaken" }, "create_token_external": { "title": "Accepteer nieuwe token in Hyperion UI" @@ -29,5 +40,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Hyperion-effecten om te laten zien", + "priority": "Hyperion prioriteit te gebruiken voor kleuren en effecten" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json index e411982b58a..8fed4ee2437 100644 --- a/homeassistant/components/hyperion/translations/no.json +++ b/homeassistant/components/hyperion/translations/no.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Hyperion-effekter \u00e5 vise", "priority": "Hyperion-prioritet for bruke til farger og effekter" } } diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json index 33b7c927520..67e89e817f0 100644 --- a/homeassistant/components/hyperion/translations/pl.json +++ b/homeassistant/components/hyperion/translations/pl.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "Efekty Hyperiona do pokazania", "priority": "Hyperion ma pierwsze\u0144stwo w u\u017cyciu dla kolor\u00f3w i efekt\u00f3w" } } diff --git a/homeassistant/components/hyperion/translations/ru.json b/homeassistant/components/hyperion/translations/ru.json index 9e74680a951..a6716bb74ea 100644 --- a/homeassistant/components/hyperion/translations/ru.json +++ b/homeassistant/components/hyperion/translations/ru.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "\u042d\u0444\u0444\u0435\u043a\u0442\u044b Hyperion", "priority": "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u0446\u0432\u0435\u0442\u043e\u0432 \u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u043e\u0432" } } diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json index bb8eacd5376..d9757ccc22a 100644 --- a/homeassistant/components/hyperion/translations/zh-Hant.json +++ b/homeassistant/components/hyperion/translations/zh-Hant.json @@ -45,6 +45,7 @@ "step": { "init": { "data": { + "effect_show_list": "\u986f\u793a Hyperion \u6548\u61c9", "priority": "Hyperion \u512a\u5148\u4f7f\u7528\u4e4b\u8272\u6eab\u8207\u7279\u6548" } } diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index f885c2c49d3..b1882619fda 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -8,7 +8,7 @@ from iammeter import real_time_api from iammeter.power_meter import IamMeterError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import debounce @@ -74,7 +74,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class IamMeter(CoordinatorEntity): +class IamMeter(CoordinatorEntity, SensorEntity): """Class for a sensor.""" def __init__(self, coordinator, uid, sensor_name, unit, dev_name): diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index d0667aab72a..0435645d87c 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -1,8 +1,10 @@ """Component to embed Aqualink devices.""" +from __future__ import annotations + import asyncio from functools import wraps import logging -from typing import Any, Dict +from typing import Any import aiohttp.client_exceptions from iaqualink import ( @@ -234,7 +236,7 @@ class AqualinkEntity(Entity): return self.dev.system.online @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return the device info.""" return { "identifiers": {(DOMAIN, self.unique_id)}, diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 2c26b2bc363..73988c4e523 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -1,6 +1,7 @@ """Support for Aqualink Thermostats.""" +from __future__ import annotations + import logging -from typing import List, Optional from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState from iaqualink.const import ( @@ -53,7 +54,7 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): return SUPPORT_TARGET_TEMPERATURE @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of supported HVAC modes.""" return CLIMATE_SUPPORTED_MODES @@ -119,7 +120,7 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): return self.dev.system.devices[sensor] @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" if self.sensor.state != "": return float(self.sensor.state) diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index c083aee7c1c..df8880a0e8f 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure zone component.""" -from typing import Optional +from __future__ import annotations from iaqualink import AqualinkClient, AqualinkLoginException import voluptuous as vol @@ -18,7 +18,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input: Optional[ConfigType] = None): + async def async_step_user(self, user_input: ConfigType | None = None): """Handle a flow start.""" # Supporting a single account. entries = self.hass.config_entries.async_entries(DOMAIN) @@ -46,6 +46,6 @@ class AqualinkFlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_import(self, user_input: Optional[ConfigType] = None): + async def async_step_import(self, user_input: ConfigType | None = None): """Occurs when an entry is setup through config.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 50f2a832211..eac6e2b7851 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -1,7 +1,7 @@ """Support for Aqualink temperature sensors.""" -from typing import Optional +from __future__ import annotations -from homeassistant.components.sensor import DOMAIN +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 @@ -22,7 +22,7 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkSensor(AqualinkEntity): +class HassAqualinkSensor(AqualinkEntity, SensorEntity): """Representation of a sensor.""" @property @@ -31,7 +31,7 @@ class HassAqualinkSensor(AqualinkEntity): return self.dev.label @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the measurement unit for the sensor.""" if self.dev.name.endswith("_temp"): if self.dev.system.temp_unit == "F": @@ -40,7 +40,7 @@ class HassAqualinkSensor(AqualinkEntity): return None @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the sensor.""" if self.dev.state == "": return None @@ -52,7 +52,7 @@ class HassAqualinkSensor(AqualinkEntity): return state @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of the sensor.""" if self.dev.name.endswith("_temp"): return DEVICE_CLASS_TEMPERATURE diff --git a/homeassistant/components/iaqualink/translations/he.json b/homeassistant/components/iaqualink/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/iaqualink/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index 149fee90583..dcb7b906ee3 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, diff --git a/homeassistant/components/iaqualink/translations/id.json b/homeassistant/components/iaqualink/translations/id.json new file mode 100644 index 00000000000..4591cf11e05 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan nama pengguna dan kata sandi untuk akun iAqualink Anda.", + "title": "Hubungkan ke iAqualink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/ko.json b/homeassistant/components/iaqualink/translations/ko.json index 1386480fca4..ef77daf978b 100644 --- a/homeassistant/components/iaqualink/translations/ko.json +++ b/homeassistant/components/iaqualink/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" @@ -13,7 +13,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "iAqualink \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "iAqualink\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/iaqualink/translations/nl.json b/homeassistant/components/iaqualink/translations/nl.json index fae8693ce4c..fc5b00694a1 100644 --- a/homeassistant/components/iaqualink/translations/nl.json +++ b/homeassistant/components/iaqualink/translations/nl.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "Gebruikersnaam/E-mailadres" + "username": "Gebruikersnaam" }, "description": "Voer de gebruikersnaam en het wachtwoord voor uw iAqualink-account in.", "title": "Verbinding maken met iAqualink" diff --git a/homeassistant/components/iaqualink/translations/ru.json b/homeassistant/components/iaqualink/translations/ru.json index 27531c65d9e..b7c9779e11a 100644 --- a/homeassistant/components/iaqualink/translations/ru.json +++ b/homeassistant/components/iaqualink/translations/ru.json @@ -10,9 +10,9 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", "title": "Jandy iAqualink" } } diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 4221cf635ba..a357df39e42 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -1,8 +1,9 @@ """iCloud account.""" +from __future__ import annotations + from datetime import timedelta import logging import operator -from typing import Dict, Optional from pyicloud import PyiCloudService from pyicloud.exceptions import ( @@ -95,7 +96,7 @@ class IcloudAccount: self._icloud_dir = icloud_dir - self.api: Optional[PyiCloudService] = None + self.api: PyiCloudService | None = None self._owner_fullname = None self._family_members_fullname = {} self._devices = {} @@ -114,8 +115,7 @@ class IcloudAccount: with_family=self._with_family, ) - if not self.api.is_trusted_session or self.api.requires_2fa: - # Session is no longer trusted + if self.api.requires_2fa: # Trigger a new log in to ensure the user enters the 2FA code again. raise PyiCloudFailedLoginException @@ -124,9 +124,9 @@ class IcloudAccount: # Login failed which means credentials need to be updated. _LOGGER.error( ( - "Your password for '%s' is no longer working. Go to the " + "Your password for '%s' is no longer working; Go to the " "Integrations menu and click on Configure on the discovered Apple " - "iCloud card to login again." + "iCloud card to login again" ), self._config_entry.data[CONF_USERNAME], ) @@ -162,7 +162,7 @@ class IcloudAccount: if self.api is None: return - if not self.api.is_trusted_session or self.api.requires_2fa: + if self.api.requires_2fa: self._require_reauth() return @@ -345,7 +345,7 @@ class IcloudAccount: return self._owner_fullname @property - def family_members_fullname(self) -> Dict[str, str]: + def family_members_fullname(self) -> dict[str, str]: """Return the account family members fullname.""" return self._family_members_fullname @@ -355,7 +355,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 +496,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 state_attributes(self) -> Dict[str, any]: + def exta_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 c79024c4f64..28570f3d93c 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -21,10 +21,10 @@ from .const import ( DEFAULT_GPS_ACCURACY_THRESHOLD, DEFAULT_MAX_INTERVAL, DEFAULT_WITH_FAMILY, + DOMAIN, STORAGE_KEY, STORAGE_VERSION, ) -from .const import DOMAIN # pylint: disable=unused-import CONF_TRUSTED_DEVICE = "trusted_device" CONF_VERIFICATION_CODE = "verification_code" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 59554c001ef..3dbc10bcf1b 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,5 +1,5 @@ """Support for tracking for iCloud devices.""" -from typing import Dict +from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity @@ -108,12 +108,12 @@ class IcloudTrackerEntity(TrackerEntity): return icon_for_icloud_device(self._device) @property - def device_state_attributes(self) -> Dict[str, any]: + def extra_state_attributes(self) -> dict[str, any]: """Return the device state attributes.""" return self._device.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/sensor.py b/homeassistant/components/icloud/sensor.py index 859148d8190..ddd3d54c556 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -1,11 +1,11 @@ """Support for iCloud sensors.""" -from typing import Dict +from __future__ import annotations +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.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import HomeAssistantType @@ -48,7 +48,7 @@ def add_entities(account, async_add_entities, tracked): async_add_entities(new_tracked, True) -class IcloudDeviceBatterySensor(Entity): +class IcloudDeviceBatterySensor(SensorEntity): """Representation of a iCloud device battery sensor.""" def __init__(self, account: IcloudAccount, device: IcloudDevice): @@ -91,12 +91,12 @@ class IcloudDeviceBatterySensor(Entity): ) @property - def device_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.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/translations/de.json b/homeassistant/components/icloud/translations/de.json index 64a6bcd885c..7baf9dc3917 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -15,6 +15,7 @@ "data": { "password": "Passwort" }, + "description": "Ihr zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisieren Sie Ihr Passwort, um diese Integration weiterhin zu verwenden.", "title": "Integration erneut authentifizieren" }, "trusted_device": { diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json index 71466dddc39..139d7a1e399 100644 --- a/homeassistant/components/icloud/translations/he.json +++ b/homeassistant/components/icloud/translations/he.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05d3\u05d5\u05d0\u05e8 \u05d0\u05dc\u05e7\u05d8\u05e8\u05d5\u05e0\u05d9" } } diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 2e820418e94..bb47cdd879b 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "send_verification_code": "Nem siker\u00fclt elk\u00fcldeni az ellen\u0151rz\u0151 k\u00f3dot", "validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st" }, @@ -8,7 +13,8 @@ "reauth": { "data": { "password": "Jelsz\u00f3" - } + }, + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "trusted_device": { "data": { diff --git a/homeassistant/components/icloud/translations/id.json b/homeassistant/components/icloud/translations/id.json new file mode 100644 index 00000000000..cd7abc1945d --- /dev/null +++ b/homeassistant/components/icloud/translations/id.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "no_device": "Tidak ada perangkat Anda yang mengaktifkan \"Temukan iPhone saya\"", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "send_verification_code": "Gagal mengirim kode verifikasi", + "validate_verification_code": "Gagal memverifikasi kode verifikasi Anda, coba lagi" + }, + "step": { + "reauth": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.", + "title": "Autentikasi Ulang Integrasi" + }, + "trusted_device": { + "data": { + "trusted_device": "Perangkat tepercaya" + }, + "description": "Pilih perangkat tepercaya Anda", + "title": "Perangkat tepercaya iCloud" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email", + "with_family": "Dengan keluarga" + }, + "description": "Masukkan kredensial Anda", + "title": "Kredensial iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Kode verifikasi" + }, + "description": "Masukkan kode verifikasi yang baru saja diterima dari iCloud", + "title": "Kode verifikasi iCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ko.json b/homeassistant/components/icloud/translations/ko.json index 5e02fb02993..52319b888ca 100644 --- a/homeassistant/components/icloud/translations/ko.json +++ b/homeassistant/components/icloud/translations/ko.json @@ -15,7 +15,8 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638" }, - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + "description": "\uc774\uc804\uc5d0 \uc785\ub825\ud55c {username}\uc5d0 \ub300\ud55c \ube44\ubc00\ubc88\ud638\uac00 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uacc4\uc18d \uc0ac\uc6a9\ud558\ub824\uba74 \ube44\ubc00\ubc88\ud638\ub97c \uc5c5\ub370\uc774\ud2b8\ud574\uc8fc\uc138\uc694.", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" }, "trusted_device": { "data": { diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index b150c8d5b16..7260f954c38 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Account reeds geconfigureerd", + "already_configured": "Account is al geconfigureerd", "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd", "reauth_successful": "Herauthenticatie was succesvol" }, diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json index c3e7007b1a0..9898beb3e92 100644 --- a/homeassistant/components/ifttt/translations/hu.json +++ b/homeassistant/components/ifttt/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az \u201eIFTTT Webhook kisalkalmaz\u00e1s\u201d ( {applet_url} ) \"Webk\u00e9r\u00e9s k\u00e9sz\u00edt\u00e9se\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/translations/id.json b/homeassistant/components/ifttt/translations/id.json new file mode 100644 index 00000000000..f997f39a54e --- /dev/null +++ b/homeassistant/components/ifttt/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menggunakan tindakan \"Make a web request\" dari [applet IFTTT Webhook]({applet_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan IFTTT?", + "title": "Siapkan Applet IFTTT Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/ko.json b/homeassistant/components/ifttt/translations/ko.json index bc561027fc3..8f01109da76 100644 --- a/homeassistant/components/ifttt/translations/ko.json +++ b/homeassistant/components/ifttt/translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf]({applet_url})\uc5d0\uc11c \"Make a web request\"\ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nHome Assistant\ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "IFTTT \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "IFTTT\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "IFTTT \uc6f9 \ud6c5 \uc560\ud50c\ub9bf \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 0db580701d0..314a7bdea31 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -1,7 +1,8 @@ """Support for IGN Sismologia (Earthquakes) Feeds.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional from georss_ign_sismologia_client import IgnSismologiaFeedManager import voluptuous as vol @@ -207,7 +208,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" if self._magnitude and self._region: return f"M {self._magnitude:.1f} - {self._region}" @@ -218,17 +219,17 @@ class IgnSismologiaLocationEvent(GeolocationEvent): return self._title @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -238,7 +239,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): return LENGTH_KILOMETERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index c539156b759..959d86a7cc1 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -55,7 +55,7 @@ DOMAIN = "ihc" IHC_CONTROLLER = "controller" IHC_INFO = "info" -IHC_PLATFORMS = ("binary_sensor", "light", "sensor", "switch") +PLATFORMS = ("binary_sensor", "light", "sensor", "switch") def validate_name(config): @@ -219,7 +219,7 @@ PULSE_SCHEMA = vol.Schema( def setup(hass, config): - """Set up the IHC platform.""" + """Set up the IHC integration.""" conf = config.get(DOMAIN) for index, controller_conf in enumerate(conf): if not ihc_setup(hass, config, controller_conf, index): @@ -229,7 +229,7 @@ def setup(hass, config): def ihc_setup(hass, config, conf, controller_id): - """Set up the IHC component.""" + """Set up the IHC integration.""" url = conf[CONF_URL] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] @@ -256,11 +256,11 @@ def ihc_setup(hass, config, conf, controller_id): def get_manual_configuration(hass, config, conf, ihc_controller, controller_id): """Get manual configuration for IHC devices.""" - for component in IHC_PLATFORMS: + for platform in PLATFORMS: discovery_info = {} - if component in conf: - component_setup = conf.get(component) - for sensor_cfg in component_setup: + if platform in conf: + platform_setup = conf.get(platform) + for sensor_cfg in platform_setup: name = sensor_cfg[CONF_NAME] device = { "ihc_id": sensor_cfg[CONF_ID], @@ -281,7 +281,7 @@ def get_manual_configuration(hass, config, conf, ihc_controller, controller_id): } discovery_info[name] = device if discovery_info: - discovery.load_platform(hass, component, DOMAIN, discovery_info, config) + discovery.load_platform(hass, platform, DOMAIN, discovery_info, config) def autosetup_ihc_products( @@ -304,21 +304,23 @@ def autosetup_ihc_products( except vol.Invalid as exception: _LOGGER.error("Invalid IHC auto setup data: %s", exception) return False + groups = project.findall(".//group") - for component in IHC_PLATFORMS: - component_setup = auto_setup_conf[component] - discovery_info = get_discovery_info(component_setup, groups, controller_id) + for platform in PLATFORMS: + platform_setup = auto_setup_conf[platform] + discovery_info = get_discovery_info(platform_setup, groups, controller_id) if discovery_info: - discovery.load_platform(hass, component, DOMAIN, discovery_info, config) + discovery.load_platform(hass, platform, DOMAIN, discovery_info, config) + return True -def get_discovery_info(component_setup, groups, controller_id): - """Get discovery info for specified IHC component.""" +def get_discovery_info(platform_setup, groups, controller_id): + """Get discovery info for specified IHC platform.""" discovery_data = {} for group in groups: groupname = group.attrib["name"] - for product_cfg in component_setup: + for product_cfg in platform_setup: products = group.findall(product_cfg[CONF_XPATH]) for product in products: nodes = product.findall(product_cfg[CONF_NODE]) diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 0b3dc763ca0..e351d2f38ea 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -42,7 +42,7 @@ class IHCDevice(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not self.info: return {} diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index cb1688bc7be..3348e857f51 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,6 +1,6 @@ """Support for IHC sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_UNIT_OF_MEASUREMENT -from homeassistant.helpers.entity import Entity from . import IHC_CONTROLLER, IHC_INFO from .ihcdevice import IHCDevice @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class IHCSensor(IHCDevice, Entity): +class IHCSensor(IHCDevice, SensorEntity): """Implementation of the IHC sensor.""" def __init__( diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index c68df580643..37b3bd7ff6a 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -1,10 +1,11 @@ """The Picture integration.""" +from __future__ import annotations + import asyncio import logging import pathlib import secrets import shutil -import typing from PIL import Image, ImageOps, UnidentifiedImageError from aiohttp import hdrs, web @@ -69,7 +70,7 @@ class ImageStorageCollection(collection.StorageCollection): self.async_add_listener(self._change_listener) self.image_dir = image_dir - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" data = self.CREATE_SCHEMA(dict(data)) uploaded_file: FileField = data["file"] @@ -117,11 +118,11 @@ class ImageStorageCollection(collection.StorageCollection): return media_file.stat().st_size @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_ID] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" return {**data, **self.UPDATE_SCHEMA(update_data)} diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index c8029c2e313..741fb8511a6 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==8.1.1"], + "requirements": ["pillow==8.1.2"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index e885a9ca7a9..58a6582e33c 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from typing import final import voluptuous as vol @@ -175,6 +176,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return "face" + @final @property def state_attributes(self): """Return device specific state attributes.""" @@ -206,9 +208,12 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): """ # Send events for face in faces: - if ATTR_CONFIDENCE in face and self.confidence: - if face[ATTR_CONFIDENCE] < self.confidence: - continue + if ( + ATTR_CONFIDENCE in face + and self.confidence + and face[ATTR_CONFIDENCE] < self.confidence + ): + continue face.update({ATTR_ENTITY_ID: self.entity_id}) self.hass.async_add_job(self.hass.bus.async_fire, EVENT_DETECT_FACE, face) diff --git a/homeassistant/components/image_processing/translations/id.json b/homeassistant/components/image_processing/translations/id.json index 19e3a64dcba..c186898fd07 100644 --- a/homeassistant/components/image_processing/translations/id.json +++ b/homeassistant/components/image_processing/translations/id.json @@ -1,3 +1,3 @@ { - "title": "Pengolahan gambar" + "title": "Pengolahan citra" } \ No newline at end of file diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 4917abc6028..4158d1be801 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -6,7 +6,7 @@ from aioimaplib import IMAP4_SSL, AioImapException import async_timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -62,7 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([sensor], True) -class ImapSensor(Entity): +class ImapSensor(SensorEntity): """Representation of an IMAP sensor.""" def __init__(self, name, user, password, server, port, charset, folder, search): diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index 04d4ca97c5a..cdd47d68d76 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_DATE, CONF_NAME, @@ -18,7 +18,6 @@ from homeassistant.const import ( CONTENT_TYPE_TEXT_PLAIN, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -145,7 +144,7 @@ class EmailReader: return None -class EmailContentSensor(Entity): +class EmailContentSensor(SensorEntity): """Representation of an EMail sensor.""" def __init__(self, hass, email_reader, name, allowed_senders, value_template): @@ -171,7 +170,7 @@ class EmailContentSensor(Entity): return self._message @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other state attributes for the message.""" return self._state_attributes @@ -221,9 +220,11 @@ class EmailContentSensor(Entity): elif part.get_content_type() == "text/html": if message_html is None: message_html = part.get_payload() - elif part.get_content_type().startswith("text"): - if message_untyped_text is None: - message_untyped_text = part.get_payload() + elif ( + part.get_content_type().startswith("text") + and message_untyped_text is None + ): + message_untyped_text = part.get_payload() if message_text is not None: return message_text diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index cec550d24d9..cea7244919b 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -1,6 +1,7 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" +from __future__ import annotations + import logging -from typing import Optional from aiohttp import ClientResponseError from incomfortclient import Gateway as InComfortGateway @@ -68,12 +69,12 @@ class IncomfortEntity(Entity): self._unique_id = self._name = None @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._unique_id @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the sensor.""" return self._name diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index bf1340fb235..cc8d2e24a0a 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,5 +1,7 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, @@ -40,6 +42,6 @@ class IncomfortFailed(IncomfortChild, BinarySensorEntity): return self._heater.status["is_failed"] @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" return {"fault_code": self._heater.status["fault_code"]} diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 274308efe06..e44090a0b48 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,5 +1,7 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from typing import Any, Dict, List, Optional +from __future__ import annotations + +from typing import Any from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( @@ -39,7 +41,7 @@ class InComfortClimate(IncomfortChild, ClimateEntity): self._room = room @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return {"status": self._room.status} @@ -54,17 +56,17 @@ class InComfortClimate(IncomfortChild, ClimateEntity): return HVAC_MODE_HEAT @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return [HVAC_MODE_HEAT] @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._room.room_temp @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._room.setpoint diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 80b6952c383..891cbb20be4 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -2,6 +2,6 @@ "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/integrations/incomfort", - "requirements": ["incomfort-client==0.4.0"], + "requirements": ["incomfort-client==0.4.4"], "codeowners": ["@zxdavb"] } diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 692eecf2317..a9e1faaba10 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,7 +1,9 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" -from typing import Any, Dict, Optional +from __future__ import annotations -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from typing import Any + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -38,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortSensor(IncomfortChild): +class IncomfortSensor(IncomfortChild, SensorEntity): """Representation of an InComfort/InTouch sensor device.""" def __init__(self, client, heater, name) -> None: @@ -57,17 +59,17 @@ class IncomfortSensor(IncomfortChild): self._unit_of_measurement = None @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the sensor.""" return self._heater.status[self._state_attr] @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement @@ -95,6 +97,6 @@ class IncomfortTemperature(IncomfortSensor): self._unit_of_measurement = TEMP_CELSIUS @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" return {self._attr: self._heater.status[self._attr]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index da6e6d89315..84ed0212d3b 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,7 +1,9 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict +from typing import Any from aiohttp import ClientResponseError @@ -50,7 +52,7 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): return "mdi:thermometer-lines" @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS} diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index e327f34d128..dde10ffca76 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,11 +1,14 @@ """Support for sending data to an Influx database.""" +from __future__ import annotations + +from contextlib import suppress from dataclasses import dataclass import logging import math import queue import threading import time -from typing import Any, Callable, Dict, List +from typing import Any, Callable from influxdb import InfluxDBClient, exceptions from influxdb_client import InfluxDBClient as InfluxDBClientV2 @@ -100,7 +103,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def create_influx_url(conf: Dict) -> Dict: +def create_influx_url(conf: dict) -> dict: """Build URL used from config inputs and default when necessary.""" if conf[CONF_API_VERSION] == API_VERSION_2: if CONF_SSL not in conf: @@ -125,7 +128,7 @@ def create_influx_url(conf: Dict) -> Dict: return conf -def validate_version_specific_config(conf: Dict) -> Dict: +def validate_version_specific_config(conf: dict) -> dict: """Ensure correct config fields are provided based on API version used.""" if conf[CONF_API_VERSION] == API_VERSION_2: if CONF_TOKEN not in conf: @@ -193,7 +196,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]: +def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: """Build event to json converter and add to config.""" entity_filter = convert_include_exclude_filter(conf) tags = conf.get(CONF_TAGS) @@ -208,7 +211,7 @@ def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]: conf[CONF_COMPONENT_CONFIG_GLOB], ) - def event_to_json(event: Dict) -> str: + def event_to_json(event: dict) -> str: """Convert event into json in format Influx expects.""" state = event.data.get(EVENT_NEW_STATE) if ( @@ -302,11 +305,9 @@ def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]: ) # Infinity and NaN are not valid floats in InfluxDB - try: + with suppress(KeyError, TypeError): if not math.isfinite(json[INFLUX_CONF_FIELDS][key]): del json[INFLUX_CONF_FIELDS][key] - except (KeyError, TypeError): - pass json[INFLUX_CONF_TAGS].update(tags) @@ -319,9 +320,9 @@ def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]: class InfluxClient: """An InfluxDB client wrapper for V1 or V2.""" - data_repositories: List[str] + data_repositories: list[str] write: Callable[[str], None] - query: Callable[[str, str], List[Any]] + query: Callable[[str, str], list[Any]] close: Callable[[], None] @@ -380,10 +381,8 @@ def get_influx_connection(conf, test_write=False, test_read=False): if test_write: # Try to write b"" to influx. If we can connect and creds are valid # Then invalid inputs is returned. Anything else is a broken config - try: + with suppress(ValueError): write_v2(b"") - except ValueError: - pass write_api = influx.write_api(write_options=ASYNCHRONOUS) if test_read: @@ -528,7 +527,7 @@ class InfluxThread(threading.Thread): dropped = 0 - try: + with suppress(queue.Empty): while len(json) < BATCH_BUFFER_SIZE and not self.shutdown: timeout = None if count == 0 else self.batch_timeout() item = self.queue.get(timeout=timeout) @@ -547,9 +546,6 @@ class InfluxThread(threading.Thread): else: dropped += 1 - except queue.Empty: - pass - if dropped: _LOGGER.warning(CATCHING_UP_MESSAGE, dropped) diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index ff9f6f93153..299fc595f4b 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -1,10 +1,14 @@ """InfluxDB component which allows you to get data from an Influx database.""" +from __future__ import annotations + import logging -from typing import Dict import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_API_VERSION, CONF_NAME, @@ -15,7 +19,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady, TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from . import create_influx_url, get_influx_connection, validate_version_specific_config @@ -67,7 +70,7 @@ def _merge_connection_config_into_query(conf, query): query[key] = conf[key] -def validate_query_format_for_version(conf: Dict) -> Dict: +def validate_query_format_for_version(conf: dict) -> dict: """Ensure queries are provided in correct format based on API version.""" if conf[CONF_API_VERSION] == API_VERSION_2: if CONF_QUERIES_FLUX not in conf: @@ -168,7 +171,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda _: influx.close()) -class InfluxSensor(Entity): +class InfluxSensor(SensorEntity): """Implementation of a Influxdb sensor.""" def __init__(self, hass, influx, query): diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index fbfe4cd0454..e030a530253 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import typing import voluptuous as vol @@ -17,7 +16,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -25,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -62,16 +61,16 @@ class InputBooleanStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return self.CREATE_SCHEMA(data) @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return {**data, **update_data} @@ -83,7 +82,7 @@ def is_on(hass, entity_id): 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: """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -145,14 +144,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: class InputBoolean(ToggleEntity, RestoreEntity): """Representation of a boolean input.""" - def __init__(self, config: typing.Optional[dict]): + def __init__(self, config: dict | None): """Initialize a boolean input.""" self._config = config self.editable = True self._state = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> InputBoolean: + def from_yaml(cls, config: dict) -> InputBoolean: """Return entity instance initialized from yaml storage.""" input_bool = cls(config) input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" @@ -170,7 +169,7 @@ class InputBoolean(ToggleEntity, RestoreEntity): return self._config.get(CONF_NAME) @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the entity.""" return {ATTR_EDITABLE: self.editable} @@ -209,7 +208,7 @@ class InputBoolean(ToggleEntity, RestoreEntity): self._state = False self.async_write_ha_state() - async def async_update_config(self, config: typing.Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index d01e931c5cc..5fe7e779a98 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an input boolean state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -10,8 +12,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 @@ -19,11 +20,11 @@ _LOGGER = logging.getLogger(__name__) async def _async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce input boolean states.""" cur_state = hass.states.get(state.entity_id) @@ -53,11 +54,11 @@ async def _async_reproduce_states( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" await asyncio.gather( diff --git a/homeassistant/components/input_boolean/translations/id.json b/homeassistant/components/input_boolean/translations/id.json index 4401df1f453..df890baae4a 100644 --- a/homeassistant/components/input_boolean/translations/id.json +++ b/homeassistant/components/input_boolean/translations/id.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Input boolean" diff --git a/homeassistant/components/input_boolean/translations/ko.json b/homeassistant/components/input_boolean/translations/ko.json index 712051d04a4..5d7d597f790 100644 --- a/homeassistant/components/input_boolean/translations/ko.json +++ b/homeassistant/components/input_boolean/translations/ko.json @@ -5,5 +5,5 @@ "on": "\ucf1c\uc9d0" } }, - "title": "\ub17c\ub9ac\uc785\ub825" + "title": "\ub17c\ub9ac \uc785\ub825" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index adefa36639a..68b7f9f32d5 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import datetime as py_datetime import logging -import typing import voluptuous as vol @@ -16,14 +15,14 @@ from homeassistant.const import ( CONF_NAME, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -102,7 +101,7 @@ CONFIG_SCHEMA = vol.Schema( RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input datetime.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -178,16 +177,16 @@ class DateTimeStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time)) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return self.CREATE_SCHEMA(data) @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return has_date_or_time({**data, **update_data}) @@ -196,7 +195,7 @@ class DateTimeStorageCollection(collection.StorageCollection): class InputDatetime(RestoreEntity): """Representation of a datetime input.""" - def __init__(self, config: typing.Dict) -> None: + def __init__(self, config: dict) -> None: """Initialize a select input.""" self._config = config self.editable = True @@ -230,7 +229,7 @@ class InputDatetime(RestoreEntity): ) @classmethod - def from_yaml(cls, config: typing.Dict) -> InputDatetime: + def from_yaml(cls, config: dict) -> InputDatetime: """Return entity instance initialized from yaml storage.""" input_dt = cls(config) input_dt.entity_id = f"{DOMAIN}.{config[CONF_ID]}" @@ -320,7 +319,7 @@ class InputDatetime(RestoreEntity): return self._current_datetime.strftime(FMT_TIME) @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = { ATTR_EDITABLE: self.editable, @@ -360,7 +359,7 @@ class InputDatetime(RestoreEntity): return attrs @property - def unique_id(self) -> typing.Optional[str]: + def unique_id(self) -> str | None: """Return unique id of the entity.""" return self._config[CONF_ID] @@ -394,7 +393,7 @@ class InputDatetime(RestoreEntity): ) self.async_write_ha_state() - async def async_update_config(self, config: typing.Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index cc906ac50b3..f996721eabd 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -1,11 +1,12 @@ """Reproduce an Input datetime state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable 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 homeassistant.util import dt as dt_util from . import ATTR_DATE, ATTR_DATETIME, ATTR_TIME, CONF_HAS_DATE, CONF_HAS_TIME, DOMAIN @@ -32,11 +33,11 @@ def is_valid_time(string: str) -> bool: async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -77,11 +78,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Input datetime states.""" await asyncio.gather( diff --git a/homeassistant/components/input_datetime/translations/ko.json b/homeassistant/components/input_datetime/translations/ko.json index a5984527e15..3f6a71e23d7 100644 --- a/homeassistant/components/input_datetime/translations/ko.json +++ b/homeassistant/components/input_datetime/translations/ko.json @@ -1,3 +1,3 @@ { - "title": "\ub0a0\uc9dc / \uc2dc\uac04\uc785\ub825" + "title": "\ub0a0\uc9dc/\uc2dc\uac04 \uc785\ub825" } \ No newline at end of file diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index b68e6fff45d..a895326c677 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import typing import voluptuous as vol @@ -16,14 +15,14 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType _LOGGER = logging.getLogger(__name__) @@ -113,7 +112,7 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -179,16 +178,16 @@ class NumberStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number)) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return self.CREATE_SCHEMA(data) @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return _cv_input_number({**data, **update_data}) @@ -197,14 +196,14 @@ class NumberStorageCollection(collection.StorageCollection): class InputNumber(RestoreEntity): """Representation of a slider.""" - def __init__(self, config: typing.Dict): + def __init__(self, config: dict): """Initialize an input number.""" self._config = config self.editable = True self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> InputNumber: + def from_yaml(cls, config: dict) -> InputNumber: """Return entity instance initialized from yaml storage.""" input_num = cls(config) input_num.entity_id = f"{DOMAIN}.{config[CONF_ID]}" @@ -252,12 +251,12 @@ class InputNumber(RestoreEntity): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def unique_id(self) -> typing.Optional[str]: + def unique_id(self) -> str | None: """Return unique id of the entity.""" return self._config[CONF_ID] @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_INITIAL: self._config.get(CONF_INITIAL), @@ -303,7 +302,7 @@ class InputNumber(RestoreEntity): """Decrement value.""" await self.async_set_value(max(self._current_value - self._step, self._minimum)) - async def async_update_config(self, config: typing.Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config # just in case min/max values changed diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index 5a6324c4333..a897aec2ba8 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -1,13 +1,14 @@ """Reproduce an Input number state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable import voluptuous as vol 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,11 +16,11 @@ _LOGGER = logging.getLogger(__name__) async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -53,11 +54,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Input number states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/input_number/translations/ko.json b/homeassistant/components/input_number/translations/ko.json index 68960733dd8..635e1e26b5e 100644 --- a/homeassistant/components/input_number/translations/ko.json +++ b/homeassistant/components/input_number/translations/ko.json @@ -1,3 +1,3 @@ { - "title": "\uc22b\uc790\uc785\ub825" + "title": "\uc22b\uc790 \uc785\ub825" } \ No newline at end of file diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index f6831dc3e88..374254a5052 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -2,25 +2,25 @@ from __future__ import annotations import logging -import typing import voluptuous as vol from homeassistant.const import ( ATTR_EDITABLE, + ATTR_OPTION, CONF_ICON, CONF_ID, CONF_NAME, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,6 @@ DOMAIN = "input_select" CONF_INITIAL = "initial" CONF_OPTIONS = "options" -ATTR_OPTION = "option" ATTR_OPTIONS = "options" ATTR_CYCLE = "cycle" @@ -88,7 +87,7 @@ CONFIG_SCHEMA = vol.Schema( RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -184,16 +183,16 @@ class InputSelectStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select)) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return self.CREATE_SCHEMA(data) @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return _cv_input_select({**data, **update_data}) @@ -202,14 +201,14 @@ class InputSelectStorageCollection(collection.StorageCollection): class InputSelect(RestoreEntity): """Representation of a select input.""" - def __init__(self, config: typing.Dict): + def __init__(self, config: dict): """Initialize a select input.""" self._config = config self.editable = True self._current_option = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> InputSelect: + def from_yaml(cls, config: dict) -> InputSelect: """Return entity instance initialized from yaml storage.""" input_select = cls(config) input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}" @@ -244,7 +243,7 @@ class InputSelect(RestoreEntity): return self._config.get(CONF_ICON) @property - def _options(self) -> typing.List[str]: + def _options(self) -> list[str]: """Return a list of selection options.""" return self._config[CONF_OPTIONS] @@ -254,12 +253,12 @@ class InputSelect(RestoreEntity): return self._current_option @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_OPTIONS: self._config[ATTR_OPTIONS], ATTR_EDITABLE: self.editable} @property - def unique_id(self) -> typing.Optional[str]: + def unique_id(self) -> str | None: """Return unique id for the entity.""" return self._config[CONF_ID] @@ -315,7 +314,7 @@ class InputSelect(RestoreEntity): self._config[CONF_OPTIONS] = options self.async_write_ha_state() - async def async_update_config(self, config: typing.Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index bf687738740..5ea7072e932 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -1,12 +1,13 @@ """Reproduce an Input select state.""" +from __future__ import annotations + import asyncio import logging from types import MappingProxyType -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable 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_OPTION, @@ -22,11 +23,11 @@ _LOGGER = logging.getLogger(__name__) async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -68,11 +69,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Input select states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/input_select/translations/ko.json b/homeassistant/components/input_select/translations/ko.json index 5620635d903..59b3f59f143 100644 --- a/homeassistant/components/input_select/translations/ko.json +++ b/homeassistant/components/input_select/translations/ko.json @@ -1,3 +1,3 @@ { - "title": "\uc120\ud0dd\uc785\ub825" + "title": "\uc120\ud0dd \uc785\ub825" } \ No newline at end of file diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 3f8c1d6a13e..2f9f6cb47ba 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -import typing import voluptuous as vol @@ -16,14 +15,14 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.helpers.typing import ConfigType, ServiceCallType _LOGGER = logging.getLogger(__name__) @@ -113,7 +112,7 @@ CONFIG_SCHEMA = vol.Schema( RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input text.""" component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -173,16 +172,16 @@ class InputTextStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text)) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return self.CREATE_SCHEMA(data) @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return _cv_input_text({**data, **update_data}) @@ -191,14 +190,14 @@ class InputTextStorageCollection(collection.StorageCollection): class InputText(RestoreEntity): """Represent a text box.""" - def __init__(self, config: typing.Dict): + def __init__(self, config: dict): """Initialize a text input.""" self._config = config self.editable = True self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: typing.Dict) -> InputText: + def from_yaml(cls, config: dict) -> InputText: """Return entity instance initialized from yaml storage.""" input_text = cls(config) input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" @@ -241,12 +240,12 @@ class InputText(RestoreEntity): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def unique_id(self) -> typing.Optional[str]: + def unique_id(self) -> str | None: """Return unique id for the entity.""" return self._config[CONF_ID] @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_EDITABLE: self.editable, @@ -282,7 +281,7 @@ class InputText(RestoreEntity): self._current_value = value self.async_write_ha_state() - async def async_update_config(self, config: typing.Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index abd28195d8d..ce1b7c12c46 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -1,11 +1,12 @@ """Reproduce an Input text state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable 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 @@ -13,11 +14,11 @@ _LOGGER = logging.getLogger(__name__) async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -41,11 +42,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Input text states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/input_text/translations/ko.json b/homeassistant/components/input_text/translations/ko.json index 6f8e3a04f2a..43e792dd2ad 100644 --- a/homeassistant/components/input_text/translations/ko.json +++ b/homeassistant/components/input_text/translations/ko.json @@ -1,3 +1,3 @@ { - "title": "\ubb38\uc790\uc785\ub825" + "title": "\ubb38\uc790 \uc785\ub825" } \ No newline at end of file diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 0188671e07f..509878f9613 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,5 +1,6 @@ """Support for INSTEON Modems (PLM and Hub).""" import asyncio +from contextlib import suppress import logging from pyinsteon import async_close, async_connect, devices @@ -17,7 +18,7 @@ from .const import ( CONF_UNITCODE, CONF_X10, DOMAIN, - INSTEON_COMPONENTS, + INSTEON_PLATFORMS, ON_OFF_EVENTS, ) from .schemas import convert_yaml_to_config_flow @@ -37,10 +38,8 @@ async def async_get_device_config(hass, config_entry): # Make a copy of addresses due to edge case where the list of devices could change during status update # Cannot be done concurrently due to issues with the underlying protocol. for address in list(devices): - try: + with suppress(AttributeError): await devices[address].async_status() - except AttributeError: - pass await devices.async_load(id_devices=1) for addr in devices: @@ -138,9 +137,9 @@ async def async_setup_entry(hass, entry): ) device = devices.add_x10_device(housecode, unitcode, x10_type, steps) - for component in INSTEON_COMPONENTS: + for platform in INSTEON_PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) for address in devices: diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index c699e76c4f3..7e034311a82 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -1,5 +1,5 @@ """Support for Insteon thermostat.""" -from typing import List, Optional +from __future__ import annotations from pyinsteon.constants import ThermostatMode from pyinsteon.operating_flag import CELSIUS @@ -97,7 +97,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): return TEMP_FAHRENHEIT @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> int | None: """Return the current humidity.""" return self._insteon_device.groups[HUMIDITY].value @@ -107,17 +107,17 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): return HVAC_MODES[self._insteon_device.groups[SYSTEM_MODE].value] @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return list(HVAC_MODES.values()) @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._insteon_device.groups[TEMPERATURE].value @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.HEAT: return self._insteon_device.groups[HEAT_SET_POINT].value @@ -126,31 +126,31 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): return None @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO: return self._insteon_device.groups[COOL_SET_POINT].value return None @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO: return self._insteon_device.groups[HEAT_SET_POINT].value return None @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting.""" return FAN_MODES[self._insteon_device.groups[FAN_MODE].value] @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" return list(FAN_MODES.values()) @property - def target_humidity(self) -> Optional[int]: + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" high = self._insteon_device.groups[HUMIDITY_HIGH].value low = self._insteon_device.groups[HUMIDITY_LOW].value @@ -163,7 +163,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): return 1 @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported. Need to be one of CURRENT_HVAC_*. @@ -177,9 +177,9 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): return CURRENT_HVAC_IDLE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Provide attributes for display on device card.""" - attr = super().device_state_attributes + attr = super().extra_state_attributes humidifier = "off" if self._insteon_device.groups[DEHUMIDIFYING].value: humidifier = "dehumidifying" diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index d8e17cfa03f..d9081c5b45e 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -# pylint: disable=unused-import from .const import ( CONF_HOUSECODE, CONF_HUB_VERSION, @@ -65,10 +64,10 @@ async def _async_connect(**kwargs): """Connect to the Insteon modem.""" try: await async_connect(**kwargs) - _LOGGER.info("Connected to Insteon modem.") + _LOGGER.info("Connected to Insteon modem") return True except ConnectionError: - _LOGGER.error("Could not connect to Insteon modem.") + _LOGGER.error("Could not connect to Insteon modem") return False diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index 07717dade9b..a40a0b0d4b0 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -34,7 +34,7 @@ from pyinsteon.groups import ( DOMAIN = "insteon" -INSTEON_COMPONENTS = [ +INSTEON_PLATFORMS = [ "binary_sensor", "climate", "cover", diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index f89aeb294fa..00ada3e9a58 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -39,7 +39,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity): """An INSTEON fan entity.""" @property - def percentage(self) -> str: + def percentage(self) -> int: """Return the current speed percentage.""" if self._insteon_device_group.value is None: return None diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 2234eb4750c..3f83440d690 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -75,7 +75,7 @@ class InsteonEntity(Entity): return f"{description} {self._insteon_device.address}{extension}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Provide attributes for display on device card.""" return {"insteon_address": self.address, "insteon_group": self.group} diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 8698a358b21..c43df24b4cb 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -1,6 +1,7 @@ """Schemas used by insteon component.""" +from __future__ import annotations + from binascii import Error as HexError, unhexlify -from typing import Dict from pyinsteon.address import Address from pyinsteon.constants import HC_LOOKUP @@ -51,7 +52,7 @@ from .const import ( ) -def set_default_port(schema: Dict) -> Dict: +def set_default_port(schema: dict) -> dict: """Set the default port based on the Hub version.""" # If the ip_port is found do nothing # If it is not found the set the default diff --git a/homeassistant/components/insteon/translations/he.json b/homeassistant/components/insteon/translations/he.json new file mode 100644 index 00000000000..8aabed0bfce --- /dev/null +++ b/homeassistant/components/insteon/translations/he.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "change_hub_config": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json new file mode 100644 index 00000000000..06033fb1321 --- /dev/null +++ b/homeassistant/components/insteon/translations/hu.json @@ -0,0 +1,72 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "select_single": "V\u00e1lassz egy lehet\u0151s\u00e9get" + }, + "step": { + "hubv1": { + "data": { + "host": "IP c\u00edm", + "port": "Port" + } + }, + "hubv2": { + "data": { + "host": "IP c\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Insteon Hub 2. verzi\u00f3" + }, + "plm": { + "data": { + "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + } + }, + "user": { + "data": { + "modem_type": "Modem t\u00edpusa." + }, + "description": "V\u00e1laszd ki az Insteon modem t\u00edpus\u00e1t.", + "title": "Insteon" + } + } + }, + "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "add_override": { + "title": "Insteon" + }, + "add_x10": { + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "IP c\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Insteon" + }, + "init": { + "title": "Insteon" + }, + "remove_override": { + "title": "Insteon" + }, + "remove_x10": { + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/id.json b/homeassistant/components/insteon/translations/id.json new file mode 100644 index 00000000000..efae5284150 --- /dev/null +++ b/homeassistant/components/insteon/translations/id.json @@ -0,0 +1,109 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "select_single": "Pilih satu opsi." + }, + "step": { + "hubv1": { + "data": { + "host": "Alamat IP", + "port": "Port" + }, + "description": "Konfigurasikan Insteon Hub Versio 1 (pra-2014).", + "title": "Insteon Hub Versi 1" + }, + "hubv2": { + "data": { + "host": "Alamat IP", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "description": "Konfigurasikan Insteon Hub Versi 2.", + "title": "Insteon Hub Versi 2" + }, + "plm": { + "data": { + "device": "Jalur Perangkat USB" + }, + "description": "Konfigurasikan Insteon PowerLink Modem (PLM).", + "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Jenis modem." + }, + "description": "Pilih jenis modem Insteon.", + "title": "Insteon" + } + } + }, + "options": { + "error": { + "cannot_connect": "Gagal terhubung", + "input_error": "Entri tidak valid, periksa nilai Anda.", + "select_single": "Pilih satu opsi." + }, + "step": { + "add_override": { + "data": { + "address": "Alamat perangkat (yaitu 1a2b3c)", + "cat": "Kategori perangkat (mis. 0x10)", + "subcat": "Subkategori perangkat (mis. 0x0a)" + }, + "description": "Tambahkan penimpaan nilai perangkat.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Kode rumah (a - p)", + "platform": "Platform", + "steps": "Langkah peredup (hanya untuk perangkat ringan, nilai bawaan adalah 22)", + "unitcode": "Unitcode (1 - 16)" + }, + "description": "Ubah kata sandi Insteon Hub.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Alamat IP", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "description": "Ubah informasi koneksi Insteon Hub. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi Hub itu sendiri. Untuk mengubah konfigurasi di Hub, gunakan aplikasi Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Tambahkan penimpaan nilai perangkat.", + "add_x10": "Tambahkan perangkat X10.", + "change_hub_config": "Ubah konfigurasi Hub.", + "remove_override": "Hapus penimpaan nilai perangkat.", + "remove_x10": "Hapus perangkat X10." + }, + "description": "Pilih opsi untuk dikonfigurasi.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Pilih alamat perangkat untuk dihapus" + }, + "description": "Hapus penimpaan nilai perangkat", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Pilih alamat perangkat untuk dihapus" + }, + "description": "Hapus perangkat X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json index 1cd65afc9e9..559e3351932 100644 --- a/homeassistant/components/insteon/translations/ko.json +++ b/homeassistant/components/insteon/translations/ko.json @@ -2,11 +2,11 @@ "config": { "abort": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, "step": { "hubv1": { @@ -14,7 +14,7 @@ "host": "IP \uc8fc\uc18c", "port": "\ud3ec\ud2b8" }, - "description": "Insteon Hub \ubc84\uc804 1 (2014 \ub144 \uc774\uc804)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.", + "description": "Insteon Hub \ubc84\uc804 1\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4. (2014\ub144 \uc774\uc804)", "title": "Insteon Hub \ubc84\uc804 1" }, "hubv2": { @@ -30,13 +30,15 @@ "plm": { "data": { "device": "USB \uc7a5\uce58 \uacbd\ub85c" - } + }, + "description": "Insteon PowerLinc Modem (PLM)\uc744 \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Insteon PLM" }, "user": { "data": { "modem_type": "\ubaa8\ub380 \uc720\ud615." }, - "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", + "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "Insteon" } } @@ -44,20 +46,28 @@ "options": { "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "select_single": "\uc635\uc158 \uc120\ud0dd" + "input_error": "\ud56d\ubaa9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uac12\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "select_single": "\uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, "step": { "add_override": { "data": { - "cat": "\uc7a5\uce58 \ubc94\uc8fc(\uc608: 0x10)" - } + "address": "\uae30\uae30 \uc8fc\uc18c (\uc608: 1a2b3c)", + "cat": "\uae30\uae30 \ubc94\uc8fc (\uc608: 0x10)", + "subcat": "\uae30\uae30 \ud558\uc704 \ubc94\uc8fc (\uc608: 0x0a)" + }, + "description": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.", + "title": "Insteon" }, "add_x10": { "data": { - "steps": "\ub514\uba38 \ub2e8\uacc4(\ub77c\uc774\ud2b8 \uc7a5\uce58\uc5d0\ub9cc, \uae30\ubcf8\uac12 22)", - "unitcode": "\ub2e8\uc704 \ucf54\ub4dc (1-16)" + "housecode": "\ud558\uc6b0\uc2a4 \ucf54\ub4dc (a - p)", + "platform": "\ud50c\ub7ab\ud3fc", + "steps": "\ubc1d\uae30 \uc870\uc808 \ub2e8\uacc4 (\uc870\uba85 \uae30\uae30 \uc804\uc6a9, \uae30\ubcf8\uac12 22)", + "unitcode": "\uc720\ub2db \ucf54\ub4dc (1-16)" }, - "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4." + "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4.", + "title": "Insteon" }, "change_hub_config": { "data": { @@ -65,29 +75,34 @@ "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "description": "Insteon \ud5c8\ube0c\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4. \ubcc0\uacbd\ud55c \ud6c4\uc5d0\ub294 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \ud5c8\ube0c \uc790\uccb4\uc758 \uad6c\uc131\uc740 \ubcc0\uacbd\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud5c8\ube0c\uc758 \uad6c\uc131\uc744 \ubcc0\uacbd\ud558\ub824\uba74 \ud5c8\ube0c \uc571\uc744 \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.", + "title": "Insteon" }, "init": { "data": { - "add_override": "\uc7a5\uce58 Override \ucd94\uac00", - "add_x10": "X10 \uc7a5\uce58\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.", + "add_override": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.", + "add_x10": "X10 \uae30\uae30\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.", "change_hub_config": "\ud5c8\ube0c \uad6c\uc131\uc744 \ubcc0\uacbd\ud569\ub2c8\ub2e4.", - "remove_override": "\uc7a5\uce58 Override \uc81c\uac70", - "remove_x10": "X10 \uc7a5\uce58\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4." + "remove_override": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4.", + "remove_x10": "X10 \uae30\uae30\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4." }, - "description": "\uad6c\uc131 \ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + "description": "\uad6c\uc131\ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "Insteon" }, "remove_override": { "data": { - "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + "address": "\uc81c\uac70\ud560 \uae30\uae30\uc758 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, - "description": "\uc7a5\uce58 Override \uc81c\uac70" + "description": "\uae30\uae30 \uc7ac\uc815\uc758 \uc81c\uac70\ud558\uae30", + "title": "Insteon" }, "remove_x10": { "data": { - "address": "\uc81c\uac70 \ud560 \uc7a5\uce58 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624." + "address": "\uc81c\uac70\ud560 \uae30\uae30\uc758 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, - "description": "X10 \uc7a5\uce58 \uc81c\uac70" + "description": "X10 \uae30\uae30 \uc81c\uac70\ud558\uae30", + "title": "Insteon" } } } diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 98a27fb1139..0c9191e8077 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -86,9 +86,22 @@ "change_hub_config": "Wijzig de Hub-configuratie.", "remove_override": "Verwijder een apparaatoverschrijving.", "remove_x10": "Verwijder een X10-apparaat." - } + }, + "description": "Selecteer een optie om te configureren.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Selecteer een apparaatadres om te verwijderen" + }, + "description": "Verwijder een apparaatoverschrijving", + "title": "Insteon" }, "remove_x10": { + "data": { + "address": "Selecteer een apparaatadres om te verwijderen" + }, + "description": "Een X10 apparaat verwijderen", "title": "Insteon" } } diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json index dec25f1fe4b..69b5354fe87 100644 --- a/homeassistant/components/insteon/translations/ru.json +++ b/homeassistant/components/insteon/translations/ru.json @@ -22,7 +22,7 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 2", "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 2" @@ -74,7 +74,7 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub.", "title": "Insteon" diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 6c59035adb4..0ab4ac0d2c4 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_METHOD, @@ -83,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([integral]) -class IntegrationSensor(RestoreEntity): +class IntegrationSensor(RestoreEntity, SensorEntity): """Representation of an integration sensor.""" def __init__( @@ -201,7 +201,7 @@ class IntegrationSensor(RestoreEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_SOURCE_ID: self._sensor_source_id} diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index a41161c7a6e..d58efddeb3c 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -212,7 +212,7 @@ class IntesisAC(ClimateEntity): return TEMP_CELSIUS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attrs = {} if self._outdoor_temp: diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index 4131811807a..d17014cdf0d 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -3,5 +3,5 @@ "name": "IntesisHome", "documentation": "https://www.home-assistant.io/integrations/intesishome", "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.7.5"] + "requirements": ["pyintesishome==1.7.6"] } diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index f9c682bf527..853fb0d479a 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -69,10 +69,12 @@ class iOSNotificationService(BaseNotificationService): """Send a message to the Lambda APNS gateway.""" data = {ATTR_MESSAGE: message} - if kwargs.get(ATTR_TITLE) is not None: - # Remove default title from notifications. - if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT: - data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) + # Remove default title from notifications. + if ( + kwargs.get(ATTR_TITLE) is not None + and kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT + ): + data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) targets = kwargs.get(ATTR_TARGET) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index ccbc118a681..c1442f0de9f 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,9 +1,9 @@ """Support for Home Assistant iOS app sensors.""" from homeassistant.components import ios +from homeassistant.components.sensor import SensorEntity from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from .const import DOMAIN @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(dev, True) -class IOSSensor(Entity): +class IOSSensor(SensorEntity): """Representation of an iOS sensor.""" def __init__(self, sensor_type, device_name, device): @@ -88,7 +88,7 @@ class IOSSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" device = self._device[ios.ATTR_DEVICE] device_battery = self._device[ios.ATTR_BATTERY] diff --git a/homeassistant/components/ios/translations/hu.json b/homeassistant/components/ios/translations/hu.json index f716fd36e9a..dda7af8c541 100644 --- a/homeassistant/components/ios/translations/hu.json +++ b/homeassistant/components/ios/translations/hu.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Csak egyetlen Home Assistant iOS konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Home Assistant iOS komponenst?" + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/ios/translations/id.json b/homeassistant/components/ios/translations/id.json index 46449f1c044..c8003cc0d3d 100644 --- a/homeassistant/components/ios/translations/id.json +++ b/homeassistant/components/ios/translations/id.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Hanya satu konfigurasi Home Assistant iOS yang diperlukan." + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "step": { "confirm": { - "description": "Apakah Anda ingin mengatur komponen iOS Home Assistant?" + "description": "Ingin memulai penyiapan?" } } } diff --git a/homeassistant/components/ios/translations/ko.json b/homeassistant/components/ios/translations/ko.json index f5da462c1ab..d7eaa77481f 100644 --- a/homeassistant/components/ios/translations/ko.json +++ b/homeassistant/components/ios/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/ios/translations/nl.json b/homeassistant/components/ios/translations/nl.json index 0575b4558af..78757f9f715 100644 --- a/homeassistant/components/ios/translations/nl.json +++ b/homeassistant/components/ios/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Home Assistant iOS nodig." + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { - "description": "Wilt u het Home Assistant iOS component instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py index 41c5ad35d83..04db9140122 100644 --- a/homeassistant/components/iota/__init__.py +++ b/homeassistant/components/iota/__init__.py @@ -67,7 +67,7 @@ class IotaDevice(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return {CONF_WALLET_NAME: self._name} diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py index a6f689e8c2d..62260be2410 100644 --- a/homeassistant/components/iota/sensor.py +++ b/homeassistant/components/iota/sensor.py @@ -1,6 +1,7 @@ """Support for IOTA wallet sensors.""" from datetime import timedelta +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME from . import CONF_WALLETS, IotaDevice @@ -27,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class IotaBalanceSensor(IotaDevice): +class IotaBalanceSensor(IotaDevice, SensorEntity): """Implement an IOTA sensor for displaying wallets balance.""" def __init__(self, wallet_config, iota_config): @@ -60,7 +61,7 @@ class IotaBalanceSensor(IotaDevice): self._state = self.api.get_inputs()["totalBalance"] -class IotaNodeSensor(IotaDevice): +class IotaNodeSensor(IotaDevice, SensorEntity): """Implement an IOTA sensor for displaying attributes of node.""" def __init__(self, iota_config): @@ -85,7 +86,7 @@ class IotaNodeSensor(IotaDevice): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._attr diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 749a3e83217..610ff91250f 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -1,4 +1,5 @@ """Support for Iperf3 sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -23,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info) async_add_entities(sensors, True) -class Iperf3Sensor(RestoreEntity): +class Iperf3Sensor(RestoreEntity, SensorEntity): """A Iperf3 sensor implementation.""" def __init__(self, iperf3_data, sensor_type): @@ -55,7 +56,7 @@ class Iperf3Sensor(RestoreEntity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index a00941624f5..9a4d7f932e1 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,17 +1,10 @@ """Component for the Portuguese weather service - IPMA.""" -from homeassistant.core import Config, HomeAssistant - from .config_flow import IpmaFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 DEFAULT_NAME = "ipma" -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured IPMA.""" - return True - - async def async_setup_entry(hass, config_entry): """Set up IPMA station as config entry.""" hass.async_create_task( diff --git a/homeassistant/components/ipma/translations/hu.json b/homeassistant/components/ipma/translations/hu.json index 0aeb36b0442..00ba66d2dd7 100644 --- a/homeassistant/components/ipma/translations/hu.json +++ b/homeassistant/components/ipma/translations/hu.json @@ -12,8 +12,13 @@ "name": "N\u00e9v" }, "description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet", - "title": "Hely" + "title": "Elhelyezked\u00e9s" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API v\u00e9gpont el\u00e9rhet\u0151" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/id.json b/homeassistant/components/ipma/translations/id.json new file mode 100644 index 00000000000..2f7e8324fdf --- /dev/null +++ b/homeassistant/components/ipma/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "name_exists": "Nama sudah ada" + }, + "step": { + "user": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur", + "mode": "Mode", + "name": "Nama" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Lokasi" + } + } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Titik akhir API IPMA dapat dijangkau" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/ko.json b/homeassistant/components/ipma/translations/ko.json index 3fd1a8d8ea9..826309a4ee4 100644 --- a/homeassistant/components/ipma/translations/ko.json +++ b/homeassistant/components/ipma/translations/ko.json @@ -15,5 +15,10 @@ "title": "\uc704\uce58" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API \uc5d4\ub4dc\ud3ec\uc778\ud2b8 \uc5f0\uacb0" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/nl.json b/homeassistant/components/ipma/translations/nl.json index 79248e0d064..154aafe1033 100644 --- a/homeassistant/components/ipma/translations/nl.json +++ b/homeassistant/components/ipma/translations/nl.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "latitude": "Latitude", - "longitude": "Longitude", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", "mode": "Mode", "name": "Naam" }, @@ -15,5 +15,10 @@ "title": "Locatie" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API-eindpunt bereikbaar" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 7a18da03ddc..86bde4bba6c 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -1,8 +1,10 @@ """The Internet Printing Protocol (IPP) integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any from pyipp import IPP, IPPError, Printer as IPPPrinter @@ -16,7 +18,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) 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 ( CoordinatorEntity, @@ -39,7 +40,7 @@ SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: Dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the IPP component.""" hass.data.setdefault(DOMAIN, {}) return True @@ -61,14 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][entry.entry_id] = coordinator - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady - - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -79,8 +77,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -166,7 +164,7 @@ class IPPEntity(CoordinatorEntity): return self._enabled_default @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this IPP device.""" if self._device_id is None: return None diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 3815dcf8f69..d2624931ea0 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the IPP integration.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from pyipp import ( IPP, @@ -24,13 +26,12 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import CONF_BASE_PATH, CONF_SERIAL, CONF_UUID -from .const import DOMAIN # pylint: disable=unused-import +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: HomeAssistantType, 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,8 +62,8 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): self.discovery_info = {} async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -98,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) -> dict[str, Any]: """Handle zeroconf discovery.""" port = discovery_info[CONF_PORT] zctype = discovery_info["type"] @@ -166,7 +167,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -180,7 +181,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/ipp/const.py b/homeassistant/components/ipp/const.py index a5345f4145e..d482f2d73e4 100644 --- a/homeassistant/components/ipp/const.py +++ b/homeassistant/components/ipp/const.py @@ -7,7 +7,6 @@ DOMAIN = "ipp" ATTR_COMMAND_SET = "command_set" ATTR_IDENTIFIERS = "identifiers" ATTR_INFO = "info" -ATTR_LOCATION = "location" ATTR_MANUFACTURER = "manufacturer" ATTR_MARKER_TYPE = "marker_type" ATTR_MARKER_LOW_LEVEL = "marker_low_level" diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 6991e6d19ea..83826409ed8 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -1,9 +1,12 @@ """Support for IPP sensors.""" -from datetime import timedelta -from typing import Any, Callable, Dict, List, Optional +from __future__ import annotations +from datetime import timedelta +from typing import Any, Callable + +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.const import ATTR_LOCATION, DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow @@ -12,7 +15,6 @@ from . import IPPDataUpdateCoordinator, IPPEntity from .const import ( ATTR_COMMAND_SET, ATTR_INFO, - ATTR_LOCATION, ATTR_MARKER_HIGH_LEVEL, ATTR_MARKER_LOW_LEVEL, ATTR_MARKER_TYPE, @@ -27,7 +29,7 @@ from .const import ( async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up IPP sensor based on a config entry.""" coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -51,7 +53,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class IPPSensor(IPPEntity): +class IPPSensor(IPPEntity, SensorEntity): """Defines an IPP sensor.""" def __init__( @@ -64,7 +66,7 @@ class IPPSensor(IPPEntity): icon: str, key: str, name: str, - unit_of_measurement: Optional[str] = None, + unit_of_measurement: str | None = None, ) -> None: """Initialize IPP sensor.""" self._unit_of_measurement = unit_of_measurement @@ -118,7 +120,7 @@ class IPPMarkerSensor(IPPSensor): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return { ATTR_MARKER_HIGH_LEVEL: self.coordinator.data.markers[ @@ -133,7 +135,7 @@ class IPPMarkerSensor(IPPSensor): } @property - def state(self) -> Optional[int]: + def state(self) -> int | None: """Return the state of the sensor.""" level = self.coordinator.data.markers[self.marker_index].level @@ -161,7 +163,7 @@ class IPPPrinterSensor(IPPSensor): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return { ATTR_INFO: self.coordinator.data.info.printer_info, @@ -203,6 +205,6 @@ class IPPUptimeSensor(IPPSensor): return uptime.replace(microsecond=0).isoformat() @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this sensor.""" return DEVICE_CLASS_TIMESTAMP diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 396992156c0..8c988eff551 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -14,8 +14,13 @@ "user": { "data": { "host": "Hoszt", - "port": "Port" + "port": "Port", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } + }, + "zeroconf_confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" } } } diff --git a/homeassistant/components/ipp/translations/id.json b/homeassistant/components/ipp/translations/id.json new file mode 100644 index 00000000000..c2b95751d4b --- /dev/null +++ b/homeassistant/components/ipp/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "connection_upgrade": "Gagal terhubung ke printer karena peningkatan koneksi diperlukan.", + "ipp_error": "Terjadi kesalahan IPP.", + "ipp_version_error": "Versi IPP tidak didukung oleh printer.", + "parse_error": "Gagal mengurai respons dari printer.", + "unique_id_required": "Perangkat tidak memiliki identifikasi unik yang diperlukan untuk ditemukan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "connection_upgrade": "Gagal terhubung ke printer. Coba lagi dengan mencentang opsi SSL/TLS." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Jalur relatif ke printer", + "host": "Host", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Siapkan printer Anda melalui Internet Printing Protocol (IPP) untuk diintegrasikan dengan Home Assistant.", + "title": "Tautkan printer Anda" + }, + "zeroconf_confirm": { + "description": "Ingin menyiapkan {name}?", + "title": "Printer yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ko.json b/homeassistant/components/ipp/translations/ko.json index 28e79ffe281..2ae4e08beaf 100644 --- a/homeassistant/components/ipp/translations/ko.json +++ b/homeassistant/components/ipp/translations/ko.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "flow_title": "\ud504\ub9b0\ud130: {name}", "step": { @@ -27,7 +27,7 @@ "title": "\ud504\ub9b0\ud130 \uc5f0\uacb0\ud558\uae30" }, "zeroconf_confirm": { - "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ubc1c\uacac\ub41c \ud504\ub9b0\ud130" } } diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index bcbd495be91..f3d2ee60797 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze printer is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "connection_upgrade": "Kan geen verbinding maken met de printer omdat een upgrade van de verbinding vereist is.", "ipp_error": "Er is een IPP-fout opgetreden.", @@ -18,10 +18,10 @@ "user": { "data": { "base_path": "Relatief pad naar de printer", - "host": "Host- of IP-adres", + "host": "Host", "port": "Poort", "ssl": "Printer ondersteunt communicatie via SSL / TLS", - "verify_ssl": "Printer gebruikt een correct SSL-certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Stel uw printer in via Internet Printing Protocol (IPP) om te integreren met Home Assistant.", "title": "Koppel uw printer" diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index bf1725a036a..c548a115e04 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -6,6 +6,7 @@ from functools import partial from pyiqvia import Client from pyiqvia.errors import IQVIAError +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -84,9 +85,9 @@ async def async_setup_entry(hass, entry): await asyncio.gather(*init_data_update_tasks) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -97,8 +98,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -109,7 +110,7 @@ async def async_unload_entry(hass, entry): return unload_ok -class IQVIAEntity(CoordinatorEntity): +class IQVIAEntity(CoordinatorEntity, SensorEntity): """Define a base IQVIA entity.""" def __init__(self, coordinator, entry, sensor_type, name, icon): @@ -123,7 +124,7 @@ class IQVIAEntity(CoordinatorEntity): self._type = sensor_type @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index ecd1e3c3c4b..1e90a983e45 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.helpers import aiohttp_client -from .const import CONF_ZIP_CODE, DOMAIN # pylint:disable=unused-import +from .const import CONF_ZIP_CODE, DOMAIN class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 6445b4ad91f..145972e2875 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.2", "pyiqvia==0.3.1"], + "requirements": ["numpy==1.20.2", "pyiqvia==0.3.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/iqvia/translations/hu.json b/homeassistant/components/iqvia/translations/hu.json new file mode 100644 index 00000000000..f5301e874ea --- /dev/null +++ b/homeassistant/components/iqvia/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/id.json b/homeassistant/components/iqvia/translations/id.json index a93f9aac26f..32f9b77135b 100644 --- a/homeassistant/components/iqvia/translations/id.json +++ b/homeassistant/components/iqvia/translations/id.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "invalid_zip_code": "Kode pos tidak valid" + }, "step": { "user": { "data": { "zip_code": "Kode Pos" - } + }, + "description": "Isi kode pos AS atau Kanada.", + "title": "IQVIA" } } } diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index fba69a6f578..b5ba16f8541 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -4,10 +4,9 @@ from datetime import timedelta from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTR_STATION = "Station" ATTR_ORIGIN = "Origin" @@ -64,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class IrishRailTransportSensor(Entity): +class IrishRailTransportSensor(SensorEntity): """Implementation of an irish rail public transport sensor.""" def __init__(self, data, station, direction, destination, stops_at, name): @@ -89,7 +88,7 @@ class IrishRailTransportSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._times: next_up = "None" diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 065af0bd611..c496bd66732 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -4,7 +4,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback -# pylint: disable=unused-import from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 41b3cf4c667..3133320d978 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,8 +1,8 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES @@ -20,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class IslamicPrayerTimeSensor(Entity): +class IslamicPrayerTimeSensor(SensorEntity): """Representation of an Islamic prayer time sensor.""" def __init__(self, sensor_type, client): diff --git a/homeassistant/components/islamic_prayer_times/translations/de.json b/homeassistant/components/islamic_prayer_times/translations/de.json index b06137bdb0e..b8097e9bd39 100644 --- a/homeassistant/components/islamic_prayer_times/translations/de.json +++ b/homeassistant/components/islamic_prayer_times/translations/de.json @@ -2,6 +2,21 @@ "config": { "abort": { "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du islamische Gebetszeiten einrichten?", + "title": "Islamische Gebetszeiten einrichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Gebetsberechnungsmethode" + } + } } }, "title": "Islamische Gebetszeiten" diff --git a/homeassistant/components/islamic_prayer_times/translations/hu.json b/homeassistant/components/islamic_prayer_times/translations/hu.json new file mode 100644 index 00000000000..065747fb39d --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/id.json b/homeassistant/components/islamic_prayer_times/translations/id.json new file mode 100644 index 00000000000..30eb3497847 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin menyiapkan Jadwal Sholat Islam?", + "title": "Siapkan Jadwal Sholat Islam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Metode perhitungan waktu sholat" + } + } + } + }, + "title": "Jadwal Sholat Islami" +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/ko.json b/homeassistant/components/islamic_prayer_times/translations/ko.json index 240ad6f57dc..aa0905b3612 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ko.json +++ b/homeassistant/components/islamic_prayer_times/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "user": { diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index e1f0d7a19ce..787d7471d43 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -79,7 +79,7 @@ class IssBinarySensor(BinarySensorEntity): return DEFAULT_DEVICE_CLASS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.iss_data: attrs = { diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 9fd844521d7..de43407c371 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,7 +1,8 @@ """Support the ISY-994 controllers.""" +from __future__ import annotations + import asyncio from functools import partial -from typing import Optional from urllib.parse import urlparse from pyisy import ISY @@ -31,7 +32,7 @@ from .const import ( ISY994_PROGRAMS, ISY994_VARIABLES, MANUFACTURER, - SUPPORTED_PLATFORMS, + PLATFORMS, SUPPORTED_PROGRAM_PLATFORMS, UNDO_UPDATE_LISTENER, ) @@ -67,7 +68,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the isy994 integration from YAML.""" - isy_config: Optional[ConfigType] = config.get(DOMAIN) + isy_config: ConfigType | None = config.get(DOMAIN) hass.data.setdefault(DOMAIN, {}) if not isy_config: @@ -111,7 +112,7 @@ async def async_setup_entry( hass_isy_data = hass.data[DOMAIN][entry.entry_id] hass_isy_data[ISY994_NODES] = {} - for platform in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: hass_isy_data[ISY994_NODES][platform] = [] hass_isy_data[ISY994_PROGRAMS] = {} @@ -143,7 +144,7 @@ async def async_setup_entry( https = True port = host.port or 443 else: - _LOGGER.error("isy994 host value in configuration is invalid") + _LOGGER.error("The isy994 host value in configuration is invalid") return False # Connect to ISY controller. @@ -176,7 +177,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 SUPPORTED_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) @@ -248,7 +249,7 @@ async def async_unload_entry( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in SUPPORTED_PLATFORMS + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 6355a9bcece..57b134e0900 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,6 +1,8 @@ """Support for ISY994 binary sensors.""" +from __future__ import annotations + from datetime import timedelta -from typing import Callable, Union +from typing import Callable from pyisy.constants import ( CMD_OFF, @@ -127,7 +129,7 @@ async def async_setup_entry( if ( device_class == DEVICE_CLASS_MOTION and device_type is not None - and any([device_type.startswith(t) for t in TYPE_INSTEON_MOTION]) + and any(device_type.startswith(t) for t in TYPE_INSTEON_MOTION) ): # Special cases for Insteon Motion Sensors I & II: # Some subnodes never report status until activated, so @@ -173,7 +175,7 @@ async def async_setup_entry( async_add_entities(devices) -def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str): +def _detect_device_type_and_class(node: Group | Node) -> (str, str): try: device_type = node.type except AttributeError: @@ -194,10 +196,8 @@ def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str): # Other devices (incl Insteon.) for device_class in [*BINARY_SENSOR_DEVICE_TYPES_ISY]: if any( - [ - device_type.startswith(t) - for t in set(BINARY_SENSOR_DEVICE_TYPES_ISY[device_class]) - ] + device_type.startswith(t) + for t in set(BINARY_SENSOR_DEVICE_TYPES_ISY[device_class]) ): return device_class, device_type return (None, device_type) @@ -281,15 +281,17 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): """ self._negative_node = child - if self._negative_node.status != ISY_VALUE_UNKNOWN: - # If the negative node has a value, it means the negative node is - # in use for this device. Next we need to check to see if the - # negative and positive nodes disagree on the state (both ON or - # both OFF). - if self._negative_node.status == self._node.status: - # The states disagree, therefore we cannot determine the state - # of the sensor until we receive our first ON event. - self._computed_state = None + # If the negative node has a value, it means the negative node is + # in use for this device. Next we need to check to see if the + # negative and positive nodes disagree on the state (both ON or + # both OFF). + if ( + self._negative_node.status != ISY_VALUE_UNKNOWN + and self._negative_node.status == self._node.status + ): + # The states disagree, therefore we cannot determine the state + # of the sensor until we receive our first ON event. + self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" @@ -457,9 +459,9 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): return DEVICE_CLASS_BATTERY @property - def device_state_attributes(self): + def extra_state_attributes(self): """Get the state attributes for the device.""" - attr = super().device_state_attributes + attr = super().extra_state_attributes attr["parent_entity_id"] = self._parent_device.entity_id return attr diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index bb98c3d31bf..2c9aa52b3a7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -1,5 +1,7 @@ """Support for Insteon Thermostats via ISY994 Platform.""" -from typing import Callable, List, Optional +from __future__ import annotations + +from typing import Callable from pyisy.constants import ( CMD_CLIMATE_FAN_SETTING, @@ -114,7 +116,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return TEMP_FAHRENHEIT @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> int | None: """Return the current humidity.""" humidity = self._node.aux_properties.get(PROP_HUMIDITY) if not humidity: @@ -122,7 +124,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return int(humidity.value) @property - def hvac_mode(self) -> Optional[str]: + def hvac_mode(self) -> str | None: """Return hvac operation ie. heat, cool mode.""" hvac_mode = self._node.aux_properties.get(CMD_CLIMATE_MODE) if not hvac_mode: @@ -140,12 +142,12 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return UOM_TO_STATES[uom].get(hvac_mode.value) @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return ISY_HVAC_MODES @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" hvac_action = self._node.aux_properties.get(PROP_HEAT_COOL_STATE) if not hvac_action: @@ -153,19 +155,19 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return UOM_TO_STATES[UOM_HVAC_ACTIONS].get(hvac_action.value) @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return convert_isy_value_to_hass( self._node.status, self._uom, self._node.prec, 1 ) @property - def target_temperature_step(self) -> Optional[float]: + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" return 1.0 @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_COOL: return self.target_temperature_high @@ -174,7 +176,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return None @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" target = self._node.aux_properties.get(PROP_SETPOINT_COOL) if not target: @@ -182,7 +184,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1) @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" target = self._node.aux_properties.get(PROP_SETPOINT_HEAT) if not target: diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 3d52687bced..41e91e2ca6b 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -22,10 +22,10 @@ from .const import ( DEFAULT_SENSOR_STRING, DEFAULT_TLS_VERSION, DEFAULT_VAR_SENSOR_STRING, + DOMAIN, ISY_URL_POSTFIX, UDN_UUID_PREFIX, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -60,7 +60,7 @@ async def validate_input(hass: core.HomeAssistant, data): https = True port = host.port or 443 else: - _LOGGER.error("isy994 host value in configuration is invalid") + _LOGGER.error("The isy994 host value in configuration is invalid") raise InvalidHost # Connect to ISY controller. diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index ef75a974377..9fdef92c84f 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -129,7 +129,7 @@ DEFAULT_VAR_SENSOR_STRING = "HA." KEY_ACTIONS = "actions" KEY_STATUS = "status" -SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE] +PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE] SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a484b56b145..f3dbe579dd8 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -134,7 +134,7 @@ class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" @property - def device_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 device_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 f565383f007..183d4b31d3b 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,4 +1,6 @@ """Support for ISY994 fans.""" +from __future__ import annotations + import math from typing import Callable @@ -43,7 +45,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): """Representation of an ISY994 fan device.""" @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed percentage.""" if self._node.status == ISY_VALUE_UNKNOWN: return None @@ -97,7 +99,7 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): """Representation of an ISY994 fan program.""" @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed percentage.""" if self._node.status == ISY_VALUE_UNKNOWN: return None diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index e1ab689eb7a..81a74430d3a 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,5 +1,7 @@ """Sorting helpers for ISY994 device classifications.""" -from typing import Any, List, Optional, Union +from __future__ import annotations + +from typing import Any from pyisy.constants import ( ISY_VALUE_UNKNOWN, @@ -38,12 +40,12 @@ from .const import ( KEY_ACTIONS, KEY_STATUS, NODE_FILTERS, + PLATFORMS, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_EZIO2X4_SENSORS, SUBNODE_FANLINC_LIGHT, SUBNODE_IOLINC_RELAY, - SUPPORTED_PLATFORMS, SUPPORTED_PROGRAM_PLATFORMS, TYPE_CATEGORY_SENSOR_ACTUATORS, TYPE_EZIO2X4, @@ -56,7 +58,7 @@ BINARY_SENSOR_ISY_STATES = ["on", "off"] def _check_for_node_def( - hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: str = None ) -> bool: """Check if the node matches the node_def_id for any platforms. @@ -69,7 +71,7 @@ def _check_for_node_def( node_def_id = node.node_def_id - platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + platforms = PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]: hass_isy_data[ISY994_NODES][platform].append(node) @@ -79,7 +81,7 @@ def _check_for_node_def( def _check_for_insteon_type( - hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: str = None ) -> bool: """Check if the node matches the Insteon type for any platforms. @@ -94,13 +96,11 @@ def _check_for_insteon_type( return False device_type = node.type - platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + platforms = PLATFORMS if not single_platform else [single_platform] for platform in platforms: if any( - [ - device_type.startswith(t) - for t in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE]) - ] + device_type.startswith(t) + for t in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE]) ): # Hacky special-cases for certain devices with different platforms @@ -146,7 +146,7 @@ def _check_for_insteon_type( def _check_for_zwave_cat( - hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: str = None ) -> bool: """Check if the node matches the ISY Z-Wave Category for any platforms. @@ -161,13 +161,11 @@ def _check_for_zwave_cat( return False device_type = node.zwave_props.category - platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + platforms = PLATFORMS if not single_platform else [single_platform] for platform in platforms: if any( - [ - device_type.startswith(t) - for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) - ] + device_type.startswith(t) + for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) ): hass_isy_data[ISY994_NODES][platform].append(node) @@ -178,7 +176,7 @@ def _check_for_zwave_cat( def _check_for_uom_id( hass_isy_data: dict, - node: Union[Group, Node], + node: Group | Node, single_platform: str = None, uom_list: list = None, ) -> bool: @@ -202,7 +200,7 @@ def _check_for_uom_id( return True return False - platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + platforms = PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom in NODE_FILTERS[platform][FILTER_UOM]: hass_isy_data[ISY994_NODES][platform].append(node) @@ -213,7 +211,7 @@ def _check_for_uom_id( def _check_for_states_in_uom( hass_isy_data: dict, - node: Union[Group, Node], + node: Group | Node, single_platform: str = None, states_list: list = None, ) -> bool: @@ -239,7 +237,7 @@ def _check_for_states_in_uom( return True return False - platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + platforms = PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]): hass_isy_data[ISY994_NODES][platform].append(node) @@ -248,7 +246,7 @@ def _check_for_states_in_uom( return False -def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Union[Group, Node]) -> bool: +def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: """Determine if the given sensor node should be a binary_sensor.""" if _check_for_node_def(hass_isy_data, node, single_platform=BINARY_SENSOR): return True @@ -328,7 +326,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: actions = None status = entity_folder.get_by_name(KEY_STATUS) - if not status or not status.protocol == PROTO_PROGRAM: + if not status or status.protocol != PROTO_PROGRAM: _LOGGER.warning( "Program %s entity '%s' not loaded, invalid/missing status program", platform, @@ -338,7 +336,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: if platform != BINARY_SENSOR: actions = entity_folder.get_by_name(KEY_ACTIONS) - if not actions or not actions.protocol == PROTO_PROGRAM: + if not actions or actions.protocol != PROTO_PROGRAM: _LOGGER.warning( "Program %s entity '%s' not loaded, invalid/missing actions program", platform, @@ -368,7 +366,7 @@ def _categorize_variables( async def migrate_old_unique_ids( - hass: HomeAssistantType, platform: str, devices: Optional[List[Any]] + hass: HomeAssistantType, platform: str, devices: list[Any] | None ) -> None: """Migrate to new controller-specific unique ids.""" registry = await async_get_registry(hass) @@ -400,11 +398,11 @@ async def migrate_old_unique_ids( def convert_isy_value_to_hass( - value: Union[int, float, None], + value: int | float | None, uom: str, - precision: Union[int, str], - fallback_precision: Optional[int] = None, -) -> Union[float, int]: + precision: int | str, + fallback_precision: int | None = None, +) -> float | int: """Fix ISY Reported Values. ISY provides float values as an integer and precision component. diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 7ff44863f6b..7f35e96acaf 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,5 +1,7 @@ """Support for ISY994 lights.""" -from typing import Callable, Dict +from __future__ import annotations + +from typing import Callable from pyisy.constants import ISY_VALUE_UNKNOWN @@ -98,9 +100,9 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): _LOGGER.debug("Unable to turn on light") @property - def device_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Return the light attributes.""" - attribs = super().device_state_attributes + attribs = super().extra_state_attributes attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness return attribs diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index d3d192b1b3b..2927fbb62b1 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,9 +1,11 @@ """Support for ISY994 sensors.""" -from typing import Callable, Dict, Union +from __future__ import annotations + +from typing import Callable from pyisy.constants import ISY_VALUE_UNKNOWN -from homeassistant.components.sensor import DOMAIN as SENSOR +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 @@ -43,11 +45,11 @@ async def async_setup_entry( async_add_entities(devices) -class ISYSensorEntity(ISYNodeEntity): +class ISYSensorEntity(ISYNodeEntity, SensorEntity): """Representation of an ISY994 sensor device.""" @property - def raw_unit_of_measurement(self) -> Union[dict, str]: + def raw_unit_of_measurement(self) -> dict | str: """Get the raw unit of measurement for the ISY994 sensor device.""" uom = self._node.uom @@ -103,7 +105,7 @@ class ISYSensorEntity(ISYNodeEntity): return raw_units -class ISYSensorVariableEntity(ISYEntity): +class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY994 variable as a sensor device.""" def __init__(self, vname: str, vobj: object) -> None: @@ -117,7 +119,7 @@ class ISYSensorVariableEntity(ISYEntity): return convert_isy_value_to_hass(self._node.status, "", self._node.prec) @property - def device_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Get the state attributes for the device.""" return { "init_value": convert_isy_value_to_hass( diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 3e9e0ae1d29..39966a9d994 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -27,7 +27,7 @@ from .const import ( ISY994_NODES, ISY994_PROGRAMS, ISY994_VARIABLES, - SUPPORTED_PLATFORMS, + PLATFORMS, SUPPORTED_PROGRAM_PLATFORMS, ) @@ -174,7 +174,7 @@ def async_setup_services(hass: HomeAssistantType): for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and not isy_name == isy.configuration["name"]: + if isy_name and isy_name != isy.configuration["name"]: continue # If an address is provided, make sure we query the correct ISY. # Otherwise, query the whole system on all ISY's connected. @@ -199,7 +199,7 @@ def async_setup_services(hass: HomeAssistantType): for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and not isy_name == isy.configuration["name"]: + if isy_name and isy_name != isy.configuration["name"]: continue if not hasattr(isy, "networking") or isy.networking is None: continue @@ -224,7 +224,7 @@ def async_setup_services(hass: HomeAssistantType): for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and not isy_name == isy.configuration["name"]: + if isy_name and isy_name != isy.configuration["name"]: continue program = None if address: @@ -247,7 +247,7 @@ def async_setup_services(hass: HomeAssistantType): for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and not isy_name == isy.configuration["name"]: + if isy_name and isy_name != isy.configuration["name"]: continue variable = None if name: @@ -279,7 +279,7 @@ def async_setup_services(hass: HomeAssistantType): hass_isy_data = hass.data[DOMAIN][config_entry_id] uuid = hass_isy_data[ISY994_ISY].configuration["uuid"] - for platform in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: for node in hass_isy_data[ISY994_NODES][platform]: if hasattr(node, "address"): current_unique_ids.append(f"{uuid}_{node.address}") diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 6177c39b231..427a51157bf 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -6,15 +6,24 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { "user": { "data": { + "host": "URL", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } + }, + "options": { + "step": { + "init": { + "title": "ISY994 Be\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json new file mode 100644 index 00000000000..fec6d1090b0 --- /dev/null +++ b/homeassistant/components/isy994/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_host": "Entri host tidak dalam format URL lengkap, misalnya, http://192.168.10.100:80", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Universal Devices ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "host": "URL", + "password": "Kata Sandi", + "tls": "Versi TLS dari pengontrol ISY.", + "username": "Nama Pengguna" + }, + "description": "Entri host harus dalam format URL lengkap, misalnya, http://192.168.10.100:80", + "title": "Hubungkan ke ISY994 Anda" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Abaikan String", + "restore_light_state": "Pulihkan Kecerahan Cahaya", + "sensor_string": "String Sensor Node", + "variable_sensor_string": "String Sensor Variabel" + }, + "description": "Mengatur opsi untuk Integrasi ISY: \n \u2022 String Sensor Node: Setiap perangkat atau folder yang berisi 'String Sensor Node' dalam nama akan diperlakukan sebagai sensor atau sensor biner. \n \u2022 Abaikan String: Setiap perangkat dengan 'Abaikan String' dalam nama akan diabaikan. \n \u2022 String Sensor Variabel: Variabel apa pun yang berisi 'String Sensor Variabel' akan ditambahkan sebagai sensor. \n \u2022 Pulihkan Kecerahan Cahaya: Jika diaktifkan, kecerahan sebelumnya akan dipulihkan saat menyalakan lampu alih-alih bawaan perangkat On-Level.", + "title": "Opsi ISY994" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ko.json b/homeassistant/components/isy994/translations/ko.json index b8c80f1ee74..29829e066b9 100644 --- a/homeassistant/components/isy994/translations/ko.json +++ b/homeassistant/components/isy994/translations/ko.json @@ -19,7 +19,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\ud638\uc2a4\ud2b8 \ud56d\ubaa9\uc740 \uc644\uc804\ud55c URL \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: http://192.168.10.100:80", - "title": "ISY994 \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "ISY994\uc5d0 \uc5f0\uacb0\ud558\uae30" } } }, diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index d263815158a..9fed7b8c99c 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -29,7 +29,8 @@ "data": { "ignore_string": "Tekenreeks negeren", "restore_light_state": "Herstel lichthelderheid", - "sensor_string": "Node Sensor String" + "sensor_string": "Node Sensor String", + "variable_sensor_string": "Variabele sensor string" }, "description": "Stel de opties in voor de ISY-integratie:\n \u2022 Node Sensor String: elk apparaat of elke map die 'Node Sensor String' in de naam bevat, wordt behandeld als een sensor of binaire sensor.\n \u2022 Ignore String: elk apparaat met 'Ignore String' in de naam wordt genegeerd.\n \u2022 Variabele sensorreeks: elke variabele die 'Variabele sensorreeks' bevat, wordt als sensor toegevoegd.\n \u2022 Lichthelderheid herstellen: indien ingeschakeld, wordt de vorige helderheid hersteld wanneer u een lamp inschakelt in plaats van het ingebouwde Aan-niveau van het apparaat.", "title": "ISY994-opties" diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index cbf88574e1e..50aa75cab37 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -16,7 +16,7 @@ "host": "URL-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "tls": "\u0412\u0435\u0440\u0441\u0438\u044f TLS \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 776d3f120c9..d509896e841 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -1,8 +1,10 @@ """Support for the iZone HVAC.""" +from __future__ import annotations + import logging -from typing import List, Optional from pizone import Controller, Zone +import voluptuous as vol from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -30,6 +32,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import 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 @@ -54,6 +57,17 @@ _IZONE_FAN_TO_HA = { Controller.Fan.AUTO: FAN_AUTO, } +ATTR_AIRFLOW = "airflow" + +IZONE_SERVICE_AIRFLOW_MIN = "airflow_min" +IZONE_SERVICE_AIRFLOW_MAX = "airflow_max" + +IZONE_SERVICE_AIRFLOW_SCHEMA = { + vol.Required(ATTR_AIRFLOW): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100), msg="invalid airflow" + ), +} + async def async_setup_entry( hass: HomeAssistantType, config: ConfigType, async_add_entities @@ -64,7 +78,7 @@ async def async_setup_entry( @callback def init_controller(ctrl: Controller): """Register the controller device and the containing zones.""" - conf = hass.data.get(DATA_CONFIG) # type: ConfigType + conf: ConfigType = hass.data.get(DATA_CONFIG) # Filter out any entities excluded in the config file if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: @@ -83,6 +97,18 @@ async def async_setup_entry( # connect to register any further components async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + IZONE_SERVICE_AIRFLOW_MIN, + IZONE_SERVICE_AIRFLOW_SCHEMA, + "async_set_airflow_min", + ) + platform.async_register_entity_service( + IZONE_SERVICE_AIRFLOW_MAX, + IZONE_SERVICE_AIRFLOW_SCHEMA, + "async_set_airflow_max", + ) + return True @@ -256,7 +282,7 @@ class ControllerDevice(ClimateEntity): return PRECISION_TENTHS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" return { "supply_temperature": show_temp( @@ -298,7 +324,7 @@ class ControllerDevice(ClimateEntity): @property @_return_on_connection_error([]) - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available operation modes.""" if self._controller.free_air: return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] @@ -320,7 +346,7 @@ class ControllerDevice(ClimateEntity): @property @_return_on_connection_error() - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" if self._controller.mode == Controller.Mode.FREE_AIR: return self._controller.temp_supply @@ -338,7 +364,7 @@ class ControllerDevice(ClimateEntity): return zone.name @property - def control_zone_setpoint(self) -> Optional[float]: + def control_zone_setpoint(self) -> float | None: """Return the temperature setpoint of the zone that currently controls the AC unit (if target temp not set by controller).""" if self._supported_features & SUPPORT_TARGET_TEMPERATURE: return None @@ -350,7 +376,7 @@ class ControllerDevice(ClimateEntity): @property @_return_on_connection_error() - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach (either from control zone or master unit).""" if self._supported_features & SUPPORT_TARGET_TEMPERATURE: return self._controller.temp_setpoint @@ -362,17 +388,17 @@ class ControllerDevice(ClimateEntity): return self._controller.temp_supply @property - def target_temperature_step(self) -> Optional[float]: + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" return 0.5 @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting.""" return _IZONE_FAN_TO_HA[self._controller.fan] @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" return list(self._fan_to_pizone) @@ -572,6 +598,30 @@ class ZoneDevice(ClimateEntity): """Return the maximum temperature.""" return self._controller.max_temp + @property + def airflow_min(self): + """Return the minimum air flow.""" + return self._zone.airflow_min + + @property + def airflow_max(self): + """Return the maximum air flow.""" + return self._zone.airflow_max + + async def async_set_airflow_min(self, **kwargs): + """Set new airflow minimum.""" + await self._controller.wrap_and_catch( + self._zone.set_airflow_min(int(kwargs[ATTR_AIRFLOW])) + ) + self.async_write_ha_state() + + async def async_set_airflow_max(self, **kwargs): + """Set new airflow maximum.""" + await self._controller.wrap_and_catch( + self._zone.set_airflow_max(int(kwargs[ATTR_AIRFLOW])) + ) + self.async_write_ha_state() + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if self._zone.mode != Zone.Mode.AUTO: @@ -610,8 +660,10 @@ class ZoneDevice(ClimateEntity): return self._zone.index @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" return { + "airflow_max": self._zone.airflow_max, + "airflow_min": self._zone.airflow_min, "zone_index": self.zone_index, } diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index a64356051d0..bb647f6273c 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -1,6 +1,7 @@ """Config flow for izone.""" import asyncio +from contextlib import suppress import logging from async_timeout import timeout @@ -28,11 +29,9 @@ async def _async_has_devices(hass): disco = await async_start_discovery_service(hass) - try: + with suppress(asyncio.TimeoutError): async with timeout(TIMEOUT_DISCOVERY): await controller_ready.wait() - except asyncio.TimeoutError: - pass if not disco.pi_disco.controllers: await async_stop_discovery_service(hass) diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 479ac496906..bed7654b7e8 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,7 +2,7 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.1.3"], + "requirements": ["python-izone==1.1.4"], "codeowners": ["@Swamp-Ig"], "config_flow": true, "homekit": { diff --git a/homeassistant/components/izone/services.yaml b/homeassistant/components/izone/services.yaml new file mode 100644 index 00000000000..d03ad66421a --- /dev/null +++ b/homeassistant/components/izone/services.yaml @@ -0,0 +1,40 @@ +airflow_min: + name: Set minimum airflow + description: Set the airflow minimum percent for a zone + target: + entity: + integration: izone + domain: climate + fields: + airflow: + name: Percent + description: Airflow percent in 5% increments + required: true + example: 95 + selector: + number: + min: 0 + max: 100 + step: 5 + unit_of_measurement: "%" + mode: slider +airflow_max: + name: Set maximum airflow + description: Set the airflow maximum percent for a zone + target: + entity: + integration: izone + domain: climate + fields: + airflow: + name: Percent + description: Airflow percent in 5% increments + required: true + example: 95 + selector: + number: + min: 0 + max: 100 + step: 5 + unit_of_measurement: "%" + mode: slider diff --git a/homeassistant/components/izone/translations/hu.json b/homeassistant/components/izone/translations/hu.json index 026093232a3..2d474986415 100644 --- a/homeassistant/components/izone/translations/hu.json +++ b/homeassistant/components/izone/translations/hu.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "step": { "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?" diff --git a/homeassistant/components/izone/translations/id.json b/homeassistant/components/izone/translations/id.json new file mode 100644 index 00000000000..208b59dc9ac --- /dev/null +++ b/homeassistant/components/izone/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan iZone?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/ko.json b/homeassistant/components/izone/translations/ko.json index b6eae170bec..d7173fc2db4 100644 --- a/homeassistant/components/izone/translations/ko.json +++ b/homeassistant/components/izone/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/izone/translations/nl.json b/homeassistant/components/izone/translations/nl.json index 22d1a3c4963..b70bb738df0 100644 --- a/homeassistant/components/izone/translations/nl.json +++ b/homeassistant/components/izone/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Geen iZone-apparaten gevonden op het netwerk.", - "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van iZone nodig." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index d1474c3cf5f..35c1505561d 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,5 +1,5 @@ """The jewish_calendar component.""" -from typing import Optional +from __future__ import annotations import hdate import voluptuous as vol @@ -78,8 +78,8 @@ CONFIG_SCHEMA = vol.Schema( def get_unique_prefix( location: hdate.Location, language: str, - candle_lighting_offset: Optional[int], - havdalah_offset: Optional[int], + candle_lighting_offset: int | None, + havdalah_offset: int | None, ) -> str: """Create a prefix for unique ids.""" config_properties = [ diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 6881f29b963..5690cd35a03 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -3,8 +3,8 @@ import logging import hdate +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TIMESTAMP, SUN_EVENT_SUNSET -from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class JewishCalendarSensor(Entity): +class JewishCalendarSensor(SensorEntity): """Representation of an Jewish calendar sensor.""" def __init__(self, data, sensor, sensor_info): @@ -111,7 +111,7 @@ class JewishCalendarSensor(Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._type != "holiday": return {} @@ -153,7 +153,7 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): return DEVICE_CLASS_TIMESTAMP @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {} diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py index a65a7ffd7fe..1331afbfe97 100644 --- a/homeassistant/components/joaoapps_join/__init__.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -121,10 +121,9 @@ def setup(hass, config): device_names = device.get(CONF_DEVICE_NAMES) name = device.get(CONF_NAME) name = f"{name.lower().replace(' ', '_')}_" if name else "" - if api_key: - if not get_devices(api_key): - _LOGGER.error("Error connecting to Join, check API key") - return False + if api_key and not get_devices(api_key): + _LOGGER.error("Error connecting to Join, check API key") + return False if device_id is None and device_ids is None and device_names is None: _LOGGER.error( "No device was provided. Please specify device_id" diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 7ba089e5dab..06cad45bdde 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -35,10 +35,9 @@ def get_service(hass, config, discovery_info=None): device_id = config.get(CONF_DEVICE_ID) device_ids = config.get(CONF_DEVICE_IDS) device_names = config.get(CONF_DEVICE_NAMES) - if api_key: - if not get_devices(api_key): - _LOGGER.error("Error connecting to Join. Check the API key") - return False + if api_key and not get_devices(api_key): + _LOGGER.error("Error connecting to Join. Check the API key") + return False if device_id is None and device_ids is None and device_names is None: _LOGGER.error( "No device was provided. Please specify device_id" diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 080df7be6bf..a7fb5e6b9b5 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -89,11 +89,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): JUICENET_COORDINATOR: coordinator, } - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -104,8 +104,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py index 5ea7e4f267a..de814be2072 100644 --- a/homeassistant/components/juicenet/config_flow.py +++ b/homeassistant/components/juicenet/config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 10008f30e7c..d908dc069ef 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, ENERGY_WATT_HOUR, @@ -7,7 +8,6 @@ from homeassistant.const import ( TIME_SECONDS, VOLT, ) -from homeassistant.helpers.entity import Entity from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR from .entity import JuiceNetDevice @@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class JuiceNetSensorDevice(JuiceNetDevice, Entity): +class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): """Implementation of a JuiceNet sensor.""" def __init__(self, device, sensor_type, coordinator): diff --git a/homeassistant/components/juicenet/translations/de.json b/homeassistant/components/juicenet/translations/de.json index 7a6b5cff541..fbdea4c321f 100644 --- a/homeassistant/components/juicenet/translations/de.json +++ b/homeassistant/components/juicenet/translations/de.json @@ -13,7 +13,7 @@ "data": { "api_token": "API-Token" }, - "description": "Du ben\u00f6tigst das API-Token von https://home.juice.net/Manage.", + "description": "Sie ben\u00f6tigen das API-Token von https://home.juice.net/Manage.", "title": "Stelle eine Verbindung zu JuiceNet her" } } diff --git a/homeassistant/components/juicenet/translations/hu.json b/homeassistant/components/juicenet/translations/hu.json new file mode 100644 index 00000000000..f04a8c1e6ca --- /dev/null +++ b/homeassistant/components/juicenet/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k 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" + }, + "step": { + "user": { + "data": { + "api_token": "API Token" + }, + "title": "Csatlakoz\u00e1s a JuiceNethez" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/id.json b/homeassistant/components/juicenet/translations/id.json new file mode 100644 index 00000000000..a150b7b7bbf --- /dev/null +++ b/homeassistant/components/juicenet/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_token": "Token API" + }, + "description": "Anda akan memerlukan Token API dari https://home.juice.net/Manage.", + "title": "Hubungkan ke JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/ko.json b/homeassistant/components/juicenet/translations/ko.json index 1e1ae6aaa88..b38ddbd1f69 100644 --- a/homeassistant/components/juicenet/translations/ko.json +++ b/homeassistant/components/juicenet/translations/ko.json @@ -14,7 +14,7 @@ "api_token": "API \ud1a0\ud070" }, "description": "https://home.juice.net/Manage \uc758 API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4.", - "title": "JuiceNet \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "JuiceNet\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py index d043dc15eaf..eae14bd330e 100644 --- a/homeassistant/components/kaiterra/__init__.py +++ b/homeassistant/components/kaiterra/__init__.py @@ -25,7 +25,7 @@ from .const import ( DEFAULT_PREFERRED_UNIT, DEFAULT_SCAN_INTERVAL, DOMAIN, - KAITERRA_COMPONENTS, + PLATFORMS, ) KAITERRA_DEVICE_SCHEMA = vol.Schema( @@ -54,7 +54,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: KAITERRA_SCHEMA}, extra=vol.ALLOW_EXTRA) async def async_setup(hass, config): - """Set up the Kaiterra components.""" + """Set up the Kaiterra integration.""" conf = config[DOMAIN] scan_interval = conf[CONF_SCAN_INTERVAL] @@ -76,11 +76,11 @@ async def async_setup(hass, config): device.get(CONF_NAME) or device[CONF_TYPE], device[CONF_DEVICE_ID], ) - for component in KAITERRA_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( async_load_platform( hass, - component, + platform, DOMAIN, {CONF_NAME: device_name, CONF_DEVICE_ID: device_id}, config, diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index ae5df387884..68377d6b254 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -96,7 +96,7 @@ class KaiterraAirQuality(AirQualityEntity): return f"{self._device_id}_air_quality" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" data = {} attributes = [ diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py index 583cd60085d..f4cc5638c18 100644 --- a/homeassistant/components/kaiterra/const.py +++ b/homeassistant/components/kaiterra/const.py @@ -71,4 +71,4 @@ DEFAULT_AQI_STANDARD = "us" DEFAULT_PREFERRED_UNIT = [] DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) -KAITERRA_COMPONENTS = ["sensor", "air_quality"] +PLATFORMS = ["sensor", "air_quality"] diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index d9500c7a000..1e4dd0cbbca 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,7 +1,7 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import DISPATCHER_KAITERRA, DOMAIN @@ -25,7 +25,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class KaiterraSensor(Entity): +class KaiterraSensor(SensorEntity): """Implementation of a Kaittera sensor.""" def __init__(self, api, name, device_id, sensor): diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 764110f94b9..e1cf9bfd3ea 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -233,7 +233,7 @@ class KebaHandler(KebaKeContact): self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning( - "failsafe_timeout, failsafe_fallback and/or " - "failsafe_persist value are not correct. %s", + "Values are not correct for: failsafe_timeout, failsafe_fallback and/or " + "failsafe_persist: %s", ex, ) diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 3fed7bbf5ab..29292470155 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -71,7 +71,7 @@ class KebaBinarySensor(BinarySensorEntity): return self._is_on @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the binary sensor.""" return self._attributes diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index f7993c28393..836785490e8 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,10 +1,10 @@ """Support for KEBA charging station sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_POWER, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, ) -from homeassistant.helpers.entity import Entity from . import DOMAIN @@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class KebaSensor(Entity): +class KebaSensor(SensorEntity): """The entity class for KEBA charging stations sensors.""" def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None): @@ -114,7 +114,7 @@ class KebaSensor(Entity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the binary sensor.""" return self._attributes diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 42d747b5238..d0217b2a4f5 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -44,9 +44,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -56,8 +56,8 @@ 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 component in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(config_entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, platform) router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 9338cb05935..a832f68e017 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -1,5 +1,5 @@ """Config flow for Keenetic NDMS2.""" -from typing import List +from __future__ import annotations from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection import voluptuous as vol @@ -103,7 +103,7 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): ROUTER ] - interfaces: List[InterfaceInfo] = await self.hass.async_add_executor_job( + interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job( router.client.get_interfaces ) diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 9df222a326c..461814b1917 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -1,7 +1,8 @@ """Support for Keenetic routers as device tracker.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import List, Optional, Set from ndms2_client import Device import voluptuous as vol @@ -57,8 +58,8 @@ async def async_get_scanner(hass: HomeAssistant, config): """Import legacy configuration from YAML.""" scanner_config = config[DEVICE_TRACKER_DOMAIN] - scan_interval: Optional[timedelta] = scanner_config.get(CONF_SCAN_INTERVAL) - consider_home: Optional[timedelta] = scanner_config.get(CONF_CONSIDER_HOME) + scan_interval: timedelta | None = scanner_config.get(CONF_SCAN_INTERVAL) + consider_home: timedelta | None = scanner_config.get(CONF_CONSIDER_HOME) host: str = scanner_config[CONF_HOST] hass.data[DOMAIN][f"imported_options_{host}"] = { @@ -139,9 +140,9 @@ async def async_setup_entry( @callback -def update_items(router: KeeneticRouter, async_add_entities, tracked: Set[str]): +def update_items(router: KeeneticRouter, async_add_entities, tracked: set[str]): """Update tracked device state from the hub.""" - new_tracked: List[KeeneticTracker] = [] + new_tracked: list[KeeneticTracker] = [] for mac, device in router.last_devices.items(): if mac not in tracked: tracked.add(mac) @@ -207,7 +208,7 @@ class KeeneticTracker(ScannerEntity): return self._router.available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if self.is_connected: return { diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index 340b25ff725..049d9aab0de 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -1,7 +1,9 @@ """The Keenetic Client class.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Callable, Dict, Optional +from typing import Callable from ndms2_client import Client, ConnectionException, Device, TelnetConnection from ndms2_client.client import RouterInfo @@ -39,11 +41,11 @@ class KeeneticRouter: """Initialize the Client.""" self.hass = hass self.config_entry = config_entry - self._last_devices: Dict[str, Device] = {} - self._router_info: Optional[RouterInfo] = None - self._connection: Optional[TelnetConnection] = None - self._client: Optional[Client] = None - self._cancel_periodic_update: Optional[Callable] = None + self._last_devices: dict[str, Device] = {} + self._router_info: RouterInfo | None = None + self._connection: TelnetConnection | None = None + self._client: Client | None = None + self._cancel_periodic_update: Callable | None = None self._available = False self._progress = None @@ -165,7 +167,7 @@ class KeeneticRouter: def _update_devices(self): """Get ARP from keenetic router.""" - _LOGGER.debug("Fetching devices from router...") + _LOGGER.debug("Fetching devices from router") try: _response = self._client.get_devices( diff --git a/homeassistant/components/keenetic_ndms2/strings.json b/homeassistant/components/keenetic_ndms2/strings.json index 15629ba0f2f..0dc1c9c302f 100644 --- a/homeassistant/components/keenetic_ndms2/strings.json +++ b/homeassistant/components/keenetic_ndms2/strings.json @@ -4,7 +4,6 @@ "user": { "title": "Set up Keenetic NDMS2 Router", "data": { - "name": "Name", "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json new file mode 100644 index 00000000000..db122ec078b --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/de.json b/homeassistant/components/keenetic_ndms2/translations/de.json index 71ce0154639..cc9a3630ab1 100644 --- a/homeassistant/components/keenetic_ndms2/translations/de.json +++ b/homeassistant/components/keenetic_ndms2/translations/de.json @@ -17,5 +17,15 @@ } } } + }, + "options": { + "step": { + "user": { + "data": { + "interfaces": "Schnittstellen zum Scannen ausw\u00e4hlen", + "scan_interval": "Scanintervall" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json new file mode 100644 index 00000000000..72482de8604 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/id.json b/homeassistant/components/keenetic_ndms2/translations/id.json new file mode 100644 index 00000000000..6a427a875a0 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "title": "Siapkan Router NDMS2 Keenetik" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "consider_home": "Pertimbangkan interval rumah", + "include_arp": "Gunakan data ARP (diabaikan jika data hotspot digunakan)", + "include_associated": "Gunakan data asosiasi Wi-Fi AP (diabaikan jika data hotspot digunakan)", + "interfaces": "Pilih antarmuka untuk dipindai", + "scan_interval": "Interval pindai", + "try_hotspot": "Gunakan data 'ip hotspot' (paling akurat)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/ko.json b/homeassistant/components/keenetic_ndms2/translations/ko.json index 3281ddbe3d4..8968d9e7709 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ko.json +++ b/homeassistant/components/keenetic_ndms2/translations/ko.json @@ -14,7 +14,8 @@ "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "title": "Keenetic NDMS2 \ub77c\uc6b0\ud130 \uc124\uc815\ud558\uae30" } } }, @@ -22,7 +23,12 @@ "step": { "user": { "data": { - "scan_interval": "\uc2a4\uce94 \uac04\uaca9" + "consider_home": "\uc7ac\uc2e4 \ucd94\uce21 \uac04\uaca9", + "include_arp": "ARP \ub370\uc774\ud130 \uc0ac\uc6a9\ud558\uae30 (\ud56b\uc2a4\ud31f \ub370\uc774\ud130\uac00 \uc0ac\uc6a9\ub418\ub294 \uacbd\uc6b0 \ubb34\uc2dc\ub428)", + "include_associated": "WiFi AP \uc5f0\uacb0 \ub370\uc774\ud130 \uc0ac\uc6a9\ud558\uae30 (\ud56b\uc2a4\ud31f \ub370\uc774\ud130\uac00 \uc0ac\uc6a9\ub418\ub294 \uacbd\uc6b0 \ubb34\uc2dc\ub428)", + "interfaces": "\uc2a4\uce94\ud560 \uc778\ud130\ud398\uc774\uc2a4\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "scan_interval": "\uc2a4\uce94 \uac04\uaca9", + "try_hotspot": "'ip \ud56b\uc2a4\ud31f' \ub370\uc774\ud130 \uc0ac\uc6a9\ud558\uae30 (\uac00\uc7a5 \uc815\ud655\ud568)" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/nl.json b/homeassistant/components/keenetic_ndms2/translations/nl.json index c3c08575052..b7c89bb65e9 100644 --- a/homeassistant/components/keenetic_ndms2/translations/nl.json +++ b/homeassistant/components/keenetic_ndms2/translations/nl.json @@ -10,10 +10,12 @@ "user": { "data": { "host": "Host", + "name": "Naam", "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" - } + }, + "title": "Keenetic NDMS2 Router instellen" } } }, @@ -21,8 +23,12 @@ "step": { "user": { "data": { + "consider_home": "Overweeg thuisinterval", + "include_arp": "Gebruik ARP-gegevens (genegeerd als hotspot-gegevens worden gebruikt)", + "include_associated": "Gebruik WiFi AP-koppelingsgegevens (genegeerd als hotspot-gegevens worden gebruikt)", "interfaces": "Kies interfaces om te scannen", - "scan_interval": "Scaninterval" + "scan_interval": "Scaninterval", + "try_hotspot": "Gebruik 'ip hotspot'-gegevens (meest nauwkeurig)" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json index bfd7f6407e7..6f99453888e 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ru.json +++ b/homeassistant/components/keenetic_ndms2/translations/ru.json @@ -13,7 +13,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Keenetic NDMS2" } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index a36fe11ef65..5316568ab52 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -395,7 +395,7 @@ class KefMediaPlayer(MediaPlayerEntity): self._update_dsp_task_remover = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the DSP settings of the KEF device.""" return self._dsp or {} diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 310bd0189bd..2ada56e1c44 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,6 +1,7 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error import asyncio +from contextlib import suppress import logging import os @@ -255,10 +256,8 @@ class KeyboardRemote: async def async_stop_monitoring(self): """Stop event monitoring task and issue event.""" if self.monitor_task is not None: - try: + with suppress(OSError): await self.hass.async_add_executor_job(self.dev.ungrab) - except OSError: - pass # monitoring of the device form the event loop and closing of the # device has to occur before cancelling the task to avoid # triggering unhandled exceptions inside evdev coroutines @@ -313,10 +312,12 @@ class KeyboardRemote: self.emulate_key_hold_repeat, ) ) - elif event.value == KEY_VALUE["key_up"]: - if event.code in repeat_tasks: - repeat_tasks[event.code].cancel() - del repeat_tasks[event.code] + elif ( + event.value == KEY_VALUE["key_up"] + and event.code in repeat_tasks + ): + repeat_tasks[event.code].cancel() + del repeat_tasks[event.code] except (OSError, PermissionError, asyncio.CancelledError): # cancel key repeat tasks for task in repeat_tasks.values(): diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index 2d6322918c7..a6b1b9ada22 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -1,8 +1,8 @@ """KIRA interface to receive UDP packets from an IR-IP bridge.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_DEVICE, CONF_NAME, STATE_UNKNOWN -from homeassistant.helpers.entity import Entity from . import CONF_SENSOR, DOMAIN @@ -21,7 +21,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([KiraReceiver(device, kira)]) -class KiraReceiver(Entity): +class KiraReceiver(SensorEntity): """Implementation of a Kira Receiver.""" def __init__(self, name, kira): @@ -55,7 +55,7 @@ class KiraReceiver(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return {CONF_DEVICE: self._device} diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 047eaa1ed3c..8a0eeed83f0 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -86,7 +86,7 @@ class KiwiLock(LockEntity): return self._state == STATE_LOCKED @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" return self._device_attrs @@ -102,7 +102,7 @@ class KiwiLock(LockEntity): try: self._client.open_door(self.lock_id) except KiwiException: - _LOGGER.error("failed to open door") + _LOGGER.error("Failed to open door") else: self._state = STATE_UNLOCKED self.hass.add_job( diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index b55ab9e1c9c..241e65fbe7f 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -9,21 +9,13 @@ from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CONF_HOSTNAME, - CONF_PASSWORD, - CONF_USERNAME, - DATA_COORDINATOR, - DATA_HOST, - DATA_HUB, - DOMAIN, - MANUFACTURER, -) +from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -41,11 +33,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up kmtronic from a config entry.""" - session = aiohttp_client.async_get_clientsession(hass) auth = Auth( session, - f"http://{entry.data[CONF_HOSTNAME]}", + f"http://{entry.data[CONF_HOST]}", entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], ) @@ -70,35 +61,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_method=async_update_data, update_interval=timedelta(seconds=30), ) - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { DATA_HUB: hub, - DATA_HOST: entry.data[DATA_HOST], DATA_COORDINATOR: coordinator, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) + update_listener = entry.add_update_listener(async_update_options) + hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener + return True +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + 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 + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) if unload_ok: + update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] + update_listener() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py index 376bb64c34c..914c00ae6f2 100644 --- a/homeassistant/components/kmtronic/config_flow.py +++ b/homeassistant/components/kmtronic/config_flow.py @@ -7,23 +7,29 @@ from pykmtronic.hub import KMTronicHubAPI import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import CONF_HOSTNAME, CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_REVERSE, DOMAIN _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({CONF_HOSTNAME: str, CONF_USERNAME: str, CONF_PASSWORD: str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - session = aiohttp_client.async_get_clientsession(hass) auth = Auth( session, - f"http://{data[CONF_HOSTNAME]}", + f"http://{data[CONF_HOST]}", data[CONF_USERNAME], data[CONF_PASSWORD], ) @@ -45,6 +51,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return KMTronicOptionsFlow(config_entry) + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -72,3 +84,28 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class KMTronicOptionsFlow(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_REVERSE, + default=self.config_entry.options.get(CONF_REVERSE), + ): bool, + } + ), + ) diff --git a/homeassistant/components/kmtronic/const.py b/homeassistant/components/kmtronic/const.py index 58553217799..8b34d423724 100644 --- a/homeassistant/components/kmtronic/const.py +++ b/homeassistant/components/kmtronic/const.py @@ -2,15 +2,13 @@ DOMAIN = "kmtronic" -CONF_HOSTNAME = "host" -CONF_USERNAME = "username" -CONF_PASSWORD = "password" +CONF_REVERSE = "reverse" DATA_HUB = "hub" -DATA_HOST = "host" DATA_COORDINATOR = "coordinator" MANUFACTURER = "KMtronic" ATTR_MANUFACTURER = "manufacturer" ATTR_IDENTIFIERS = "identifiers" -ATTR_NAME = "name" + +UPDATE_LISTENER = "update_listener" diff --git a/homeassistant/components/kmtronic/strings.json b/homeassistant/components/kmtronic/strings.json index 7becb830d91..2aaa0d2f8dd 100644 --- a/homeassistant/components/kmtronic/strings.json +++ b/homeassistant/components/kmtronic/strings.json @@ -17,5 +17,14 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Reverse switch logic (use NC)" + } + } + } } } diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index 5970ec20cb8..d37cd54ce1a 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -3,19 +3,19 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DATA_COORDINATOR, DATA_HOST, DATA_HUB, DOMAIN +from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN async def async_setup_entry(hass, entry, async_add_entities): """Config entry example.""" coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB] - host = hass.data[DOMAIN][entry.entry_id][DATA_HOST] + reverse = entry.options.get(CONF_REVERSE, False) await hub.async_get_relays() async_add_entities( [ - KMtronicSwitch(coordinator, host, relay, entry.unique_id) + KMtronicSwitch(coordinator, relay, reverse, entry.entry_id) for relay in hub.relays ] ) @@ -24,17 +24,12 @@ async def async_setup_entry(hass, entry, async_add_entities): class KMtronicSwitch(CoordinatorEntity, SwitchEntity): """KMtronic Switch Entity.""" - def __init__(self, coordinator, host, relay, config_entry_id): + def __init__(self, coordinator, relay, reverse, config_entry_id): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) - self._host = host self._relay = relay self._config_entry_id = config_entry_id - - @property - def available(self) -> bool: - """Return whether the entity is available.""" - return self.coordinator.last_update_success + self._reverse = reverse @property def name(self) -> str: @@ -46,22 +41,25 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): """Return the unique ID of the entity.""" return f"{self._config_entry_id}_relay{self._relay.id}" - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return True - @property def is_on(self): """Return entity state.""" + if self._reverse: + return not self._relay.is_on return self._relay.is_on async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" - await self._relay.turn_on() + if self._reverse: + await self._relay.turn_off() + else: + await self._relay.turn_on() self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" - await self._relay.turn_off() + if self._reverse: + await self._relay.turn_on() + else: + await self._relay.turn_off() self.async_write_ha_state() diff --git a/homeassistant/components/kmtronic/translations/bg.json b/homeassistant/components/kmtronic/translations/bg.json new file mode 100644 index 00000000000..a84e1c3bfdf --- /dev/null +++ b/homeassistant/components/kmtronic/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ca.json b/homeassistant/components/kmtronic/translations/ca.json index df8218bab3e..0847a86a41b 100644 --- a/homeassistant/components/kmtronic/translations/ca.json +++ b/homeassistant/components/kmtronic/translations/ca.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "L\u00f2gica de commutaci\u00f3 inversa (utilitza NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/en.json b/homeassistant/components/kmtronic/translations/en.json index f15fe84c3ed..61da788028e 100644 --- a/homeassistant/components/kmtronic/translations/en.json +++ b/homeassistant/components/kmtronic/translations/en.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Reverse switch logic (use NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/es.json b/homeassistant/components/kmtronic/translations/es.json new file mode 100644 index 00000000000..f7c20f7805b --- /dev/null +++ b/homeassistant/components/kmtronic/translations/es.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Fallo al conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "L\u00f3gica de conmutaci\u00f3n inversa (utilizar NC)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/et.json b/homeassistant/components/kmtronic/translations/et.json index 0c1715b4932..d501e1d1962 100644 --- a/homeassistant/components/kmtronic/translations/et.json +++ b/homeassistant/components/kmtronic/translations/et.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "P\u00f6\u00f6rdl\u00fcliti loogika (kasuta NC-d)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/fr.json b/homeassistant/components/kmtronic/translations/fr.json index 45620fe7795..7c0050bfad8 100644 --- a/homeassistant/components/kmtronic/translations/fr.json +++ b/homeassistant/components/kmtronic/translations/fr.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Logique de commutation inverse (utiliser NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/hu.json b/homeassistant/components/kmtronic/translations/hu.json new file mode 100644 index 00000000000..0abcc301f0c --- /dev/null +++ b/homeassistant/components/kmtronic/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/id.json b/homeassistant/components/kmtronic/translations/id.json new file mode 100644 index 00000000000..ed8fde32106 --- /dev/null +++ b/homeassistant/components/kmtronic/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", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/it.json b/homeassistant/components/kmtronic/translations/it.json index e9356485e08..59aeaa25f6f 100644 --- a/homeassistant/components/kmtronic/translations/it.json +++ b/homeassistant/components/kmtronic/translations/it.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Invertire la logica dell'interruttore (usare NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ko.json b/homeassistant/components/kmtronic/translations/ko.json new file mode 100644 index 00000000000..cf62f34c755 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/ko.json @@ -0,0 +1,30 @@ +{ + "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", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "\uc2a4\uc704\uce58 \ubc29\uc2dd \ubcc0\uacbd (\uc0c1\uc2dc\ud3d0\ub85c(NC)\ub85c \uc0ac\uc6a9)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/nl.json b/homeassistant/components/kmtronic/translations/nl.json index 8ad15260b0d..9c32e48b20a 100644 --- a/homeassistant/components/kmtronic/translations/nl.json +++ b/homeassistant/components/kmtronic/translations/nl.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Omgekeerde schakelaarlogica (gebruik NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/no.json b/homeassistant/components/kmtronic/translations/no.json index 249711bb912..1b192f7895e 100644 --- a/homeassistant/components/kmtronic/translations/no.json +++ b/homeassistant/components/kmtronic/translations/no.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Omvendt bryterlogikk (bruk NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/pl.json b/homeassistant/components/kmtronic/translations/pl.json index 25dab56796c..6b0d06df11b 100644 --- a/homeassistant/components/kmtronic/translations/pl.json +++ b/homeassistant/components/kmtronic/translations/pl.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Odwr\u00f3\u0107 logik\u0119 prze\u0142\u0105cznika (u\u017cyj NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/pt.json b/homeassistant/components/kmtronic/translations/pt.json new file mode 100644 index 00000000000..6ff15c6c8d7 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Inverter l\u00f3gica do interruptor (usar NC)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/ru.json b/homeassistant/components/kmtronic/translations/ru.json index 9e0db9fcf94..219af38fae9 100644 --- a/homeassistant/components/kmtronic/translations/ru.json +++ b/homeassistant/components/kmtronic/translations/ru.json @@ -13,7 +13,16 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "\u041b\u043e\u0433\u0438\u043a\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c NC)" } } } diff --git a/homeassistant/components/kmtronic/translations/zh-Hant.json b/homeassistant/components/kmtronic/translations/zh-Hant.json index cad7d736a9d..5027bc2f5b2 100644 --- a/homeassistant/components/kmtronic/translations/zh-Hant.json +++ b/homeassistant/components/kmtronic/translations/zh-Hant.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "\u53cd\u5411\u958b\u95dc\u908f\u8f2f\uff08\u4f7f\u7528 NC\uff09" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index b8feb010e29..11ed7fc3c7c 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -1,4 +1,6 @@ """Support KNX devices.""" +from __future__ import annotations + import asyncio import logging @@ -17,22 +19,22 @@ from xknx.telegram import AddressFilter, GroupAddress, Telegram from xknx.telegram.apci import GroupValueRead, GroupValueResponse, GroupValueWrite from homeassistant.const import ( - CONF_ADDRESS, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) +from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ServiceCallType +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, SupportedPlatforms -from .expose import create_knx_exposure +from .const import DOMAIN, KNX_ADDRESS, SupportedPlatforms +from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure from .factory import create_knx_device from .schema import ( BinarySensorSchema, @@ -49,6 +51,7 @@ from .schema import ( WeatherSchema, ga_validator, ia_validator, + sensor_type_validator, ) _LOGGER = logging.getLogger(__name__) @@ -77,6 +80,9 @@ SERVICE_KNX_READ = "read" CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( + # deprecated since 2021.4 + cv.deprecated(CONF_KNX_CONFIG), + # deprecated since 2021.2 cv.deprecated(CONF_KNX_FIRE_EVENT), cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER), vol.Schema( @@ -148,15 +154,21 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_KNX_SEND_SCHEMA = vol.Any( vol.Schema( { - vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): vol.All( + cv.ensure_list, + [ga_validator], + ), vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, - vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), + vol.Required(SERVICE_KNX_ATTR_TYPE): sensor_type_validator, } ), vol.Schema( # without type given payload is treated as raw bytes { - vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): vol.All( + cv.ensure_list, + [ga_validator], + ), vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( cv.positive_int, [cv.positive_int] ), @@ -166,7 +178,7 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( SERVICE_KNX_READ_SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): vol.All( + vol.Required(KNX_ADDRESS): vol.All( cv.ensure_list, [ga_validator], ) @@ -175,13 +187,16 @@ SERVICE_KNX_READ_SCHEMA = vol.Schema( SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): vol.All( + cv.ensure_list, + [ga_validator], + ), vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, } ) SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( - ExposeSchema.SCHEMA.extend( + ExposeSchema.EXPOSE_SENSOR_SCHEMA.extend( { vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, } @@ -189,7 +204,7 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( vol.Schema( # for removing only `address` is required { - vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): ga_validator, vol.Required(SERVICE_KNX_ATTR_REMOVE): vol.All(cv.boolean, True), }, extra=vol.ALLOW_EXTRA, @@ -197,8 +212,8 @@ SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA = vol.Any( ) -async def async_setup(hass, config): - """Set up the KNX component.""" +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the KNX integration.""" try: knx_module = KNXModule(hass, config) hass.data[DOMAIN] = knx_module @@ -226,12 +241,6 @@ async def async_setup(hass, config): discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) ) - if not knx_module.xknx.devices: - _LOGGER.warning( - "No KNX devices are configured. Please read " - "https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes" - ) - hass.services.async_register( DOMAIN, SERVICE_KNX_SEND, @@ -262,7 +271,7 @@ async def async_setup(hass, config): schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA, ) - async def reload_service_handler(service_call: ServiceCallType) -> None: + async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all KNX components and load new ones from config.""" # First check for config file. If for some reason it is no longer there @@ -290,19 +299,19 @@ async def async_setup(hass, config): class KNXModule: """Representation of KNX Object.""" - def __init__(self, hass, config): - """Initialize of KNX module.""" + def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: + """Initialize KNX module.""" self.hass = hass self.config = config self.connected = False - self.exposures = [] - self.service_exposures = {} + self.exposures: list[KNXExposeSensor | KNXExposeTime] = [] + self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {} self.init_xknx() self._knx_event_callback: TelegramQueue.Callback = self.register_callback() - def init_xknx(self): - """Initialize of KNX object.""" + def init_xknx(self) -> None: + """Initialize XKNX object.""" self.xknx = XKNX( config=self.config_file(), own_address=self.config[DOMAIN][CONF_KNX_INDIVIDUAL_ADDRESS], @@ -313,26 +322,26 @@ class KNXModule: state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER], ) - async def start(self): - """Start KNX object. Connect to tunneling or Routing device.""" + async def start(self) -> None: + """Start XKNX object. Connect to tunneling or Routing device.""" await self.xknx.start() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) self.connected = True - async def stop(self, event): - """Stop KNX object. Disconnect from tunneling or Routing device.""" + async def stop(self, event: Event) -> None: + """Stop XKNX object. Disconnect from tunneling or Routing device.""" await self.xknx.stop() - def config_file(self): + 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 + return config_file # type: ignore - def connection_config(self): + def connection_config(self) -> ConnectionConfig: """Return the connection_config.""" if CONF_KNX_TUNNELING in self.config[DOMAIN]: return self.connection_config_tunneling() @@ -341,7 +350,7 @@ class KNXModule: # config from xknx.yaml always has priority later on return ConnectionConfig(auto_reconnect=True) - def connection_config_routing(self): + def connection_config_routing(self) -> ConnectionConfig: """Return the connection_config if routing is configured.""" local_ip = None # all configuration values are optional @@ -353,27 +362,33 @@ class KNXModule: connection_type=ConnectionType.ROUTING, local_ip=local_ip ) - def connection_config_tunneling(self): + def connection_config_tunneling(self) -> ConnectionConfig: """Return the connection_config if tunneling is configured.""" gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST] gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_PORT] local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get( ConnectionSchema.CONF_KNX_LOCAL_IP ) + route_back = self.config[DOMAIN][CONF_KNX_TUNNELING][ + ConnectionSchema.CONF_KNX_ROUTE_BACK + ] return ConnectionConfig( connection_type=ConnectionType.TUNNELING, gateway_ip=gateway_ip, gateway_port=gateway_port, local_ip=local_ip, + route_back=route_back, auto_reconnect=True, ) - async def telegram_received_cb(self, telegram): + async def telegram_received_cb(self, telegram: Telegram) -> None: """Call invoked after a KNX telegram was received.""" data = None - # Not all telegrams have serializable data. - if isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)): + if ( + isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)) + and telegram.payload.value is not None + ): data = telegram.payload.value.value self.hass.bus.async_fire( @@ -392,34 +407,39 @@ class KNXModule: address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) ) - return self.xknx.telegram_queue.register_telegram_received_cb( + return self.xknx.telegram_queue.register_telegram_received_cb( # type: ignore[no-any-return] self.telegram_received_cb, address_filters=address_filters, group_addresses=[], match_for_outgoing=True, ) - async def service_event_register_modify(self, call): + async def service_event_register_modify(self, call: ServiceCall) -> None: """Service for adding or removing a GroupAddress to the knx_event filter.""" - group_address = GroupAddress(call.data[CONF_ADDRESS]) - if call.data.get(SERVICE_KNX_ATTR_REMOVE): - try: - self._knx_event_callback.group_addresses.remove(group_address) - except ValueError: - _LOGGER.warning( - "Service event_register could not remove event for '%s'", - group_address, - ) - elif group_address not in self._knx_event_callback.group_addresses: - self._knx_event_callback.group_addresses.append(group_address) - _LOGGER.debug( - "Service event_register registered event for '%s'", - group_address, - ) + attr_address = call.data[KNX_ADDRESS] + group_addresses = map(GroupAddress, attr_address) - async def service_exposure_register_modify(self, call): + if call.data.get(SERVICE_KNX_ATTR_REMOVE): + for group_address in group_addresses: + try: + self._knx_event_callback.group_addresses.remove(group_address) + except ValueError: + _LOGGER.warning( + "Service event_register could not remove event for '%s'", + str(group_address), + ) + else: + for group_address in group_addresses: + if group_address not in self._knx_event_callback.group_addresses: + self._knx_event_callback.group_addresses.append(group_address) + _LOGGER.debug( + "Service event_register registered event for '%s'", + str(group_address), + ) + + async def service_exposure_register_modify(self, call: ServiceCall) -> None: """Service for adding or removing an exposure to KNX bus.""" - group_address = call.data.get(CONF_ADDRESS) + group_address = call.data[KNX_ADDRESS] if call.data.get(SERVICE_KNX_ATTR_REMOVE): try: @@ -434,13 +454,14 @@ class KNXModule: if group_address in self.service_exposures: replaced_exposure = self.service_exposures.pop(group_address) + assert replaced_exposure.device is not None _LOGGER.warning( "Service exposure_register replacing already registered exposure for '%s' - %s", group_address, replaced_exposure.device.name, ) replaced_exposure.shutdown() - exposure = create_knx_exposure(self.hass, self.xknx, call.data) + exposure = create_knx_exposure(self.hass, self.xknx, call.data) # type: ignore[arg-type] self.service_exposures[group_address] = exposure _LOGGER.debug( "Service exposure_register registered exposure for '%s' - %s", @@ -448,32 +469,33 @@ class KNXModule: exposure.device.name, ) - async def service_send_to_knx_bus(self, call): + async def service_send_to_knx_bus(self, call: ServiceCall) -> None: """Service for sending an arbitrary KNX message to the KNX bus.""" - attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) - attr_address = call.data.get(CONF_ADDRESS) + attr_address = call.data[KNX_ADDRESS] + attr_payload = call.data[SERVICE_KNX_ATTR_PAYLOAD] attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE) - def calculate_payload(attr_payload): - """Calculate payload depending on type of attribute.""" - if attr_type is not None: - transcoder = DPTBase.parse_transcoder(attr_type) - if transcoder is None: - raise ValueError(f"Invalid type for knx.send service: {attr_type}") - return DPTArray(transcoder.to_knx(attr_payload)) - if isinstance(attr_payload, int): - return DPTBinary(attr_payload) - return DPTArray(attr_payload) + payload: DPTBinary | DPTArray + if attr_type is not None: + transcoder = DPTBase.parse_transcoder(attr_type) + if transcoder is None: + raise ValueError(f"Invalid type for knx.send service: {attr_type}") + payload = DPTArray(transcoder.to_knx(attr_payload)) + elif isinstance(attr_payload, int): + payload = DPTBinary(attr_payload) + else: + payload = DPTArray(attr_payload) - telegram = Telegram( - destination_address=GroupAddress(attr_address), - payload=GroupValueWrite(calculate_payload(attr_payload)), - ) - await self.xknx.telegrams.put(telegram) + for address in attr_address: + telegram = Telegram( + destination_address=GroupAddress(address), + payload=GroupValueWrite(payload), + ) + await self.xknx.telegrams.put(telegram) - async def service_read_to_knx_bus(self, call): + async def service_read_to_knx_bus(self, call: ServiceCall) -> None: """Service for sending a GroupValueRead telegram to the KNX bus.""" - for address in call.data.get(CONF_ADDRESS): + for address in call.data[KNX_ADDRESS]: telegram = Telegram( destination_address=GroupAddress(address), payload=GroupValueRead(), diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index f7ec3e80fa1..0faeb9f37b4 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,15 +1,25 @@ """Support for KNX/IP binary sensors.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any, Callable, Iterable from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ATTR_COUNTER, DOMAIN from .knx_entity import KnxEntity -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up binary sensor(s) for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -21,24 +31,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXBinarySensor(KnxEntity, BinarySensorEntity): """Representation of a KNX binary sensor.""" - def __init__(self, device: XknxBinarySensor): + def __init__(self, device: XknxBinarySensor) -> None: """Initialize of KNX binary sensor.""" + self._device: XknxBinarySensor super().__init__(device) @property - def device_class(self): + def device_class(self) -> str | None: """Return the class of this sensor.""" if self._device.device_class in DEVICE_CLASSES: return self._device.device_class return None @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self._device.is_on() @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific state attributes.""" if self._device.counter is not None: return {ATTR_COUNTER: self._device.counter} diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 565c41298a3..ca3f7b0f22a 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,5 +1,7 @@ """Support for KNX/IP climate devices.""" -from typing import List, Optional +from __future__ import annotations + +from typing import Any, Callable, Iterable from xknx.devices import Climate as XknxClimate from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode @@ -13,6 +15,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +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 @@ -21,7 +26,12 @@ CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()} PRESET_MODES_INV = {value: key for key, value in PRESET_MODES.items()} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up climate(s) for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -33,8 +43,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXClimate(KnxEntity, ClimateEntity): """Representation of a KNX climate device.""" - def __init__(self, device: XknxClimate): + def __init__(self, device: XknxClimate) -> None: """Initialize of a KNX climate device.""" + self._device: XknxClimate super().__init__(device) self._unit_of_measurement = TEMP_CELSIUS @@ -44,42 +55,45 @@ class KNXClimate(KnxEntity, ClimateEntity): """Return the list of supported features.""" return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - async def async_update(self): + async def async_update(self) -> None: """Request a state update from KNX bus.""" await self._device.sync() - await self._device.mode.sync() + if self._device.mode is not None: + await self._device.mode.sync() @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.temperature.value @property - def target_temperature_step(self): + def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" return self._device.temperature_step @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device.target_temperature.value @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" - return self._device.target_temperature_min + temp = self._device.target_temperature_min + return temp if temp is not None else super().min_temp @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" - return self._device.target_temperature_max + temp = self._device.target_temperature_max + return temp if temp is not None else super().max_temp - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -88,11 +102,11 @@ class KNXClimate(KnxEntity, ClimateEntity): self.async_write_ha_state() @property - def hvac_mode(self) -> Optional[str]: + def hvac_mode(self) -> str: """Return current operation ie. heat, cool, idle.""" if self._device.supports_on_off and not self._device.is_on: return HVAC_MODE_OFF - if self._device.mode.supports_controller_mode: + if self._device.mode is not None and self._device.mode.supports_controller_mode: return CONTROLLER_MODES.get( self._device.mode.controller_mode.value, HVAC_MODE_HEAT ) @@ -100,21 +114,23 @@ class KNXClimate(KnxEntity, ClimateEntity): return HVAC_MODE_HEAT @property - def hvac_modes(self) -> Optional[List[str]]: + def hvac_modes(self) -> list[str]: """Return the list of available operation/controller modes.""" - _controller_modes = [ - CONTROLLER_MODES.get(controller_mode.value) - for controller_mode in self._device.mode.controller_modes - ] + ha_controller_modes: list[str | None] = [] + if self._device.mode is not None: + for knx_controller_mode in self._device.mode.controller_modes: + ha_controller_modes.append( + CONTROLLER_MODES.get(knx_controller_mode.value) + ) if self._device.supports_on_off: - if not _controller_modes: - _controller_modes.append(HVAC_MODE_HEAT) - _controller_modes.append(HVAC_MODE_OFF) + if not ha_controller_modes: + ha_controller_modes.append(HVAC_MODE_HEAT) + ha_controller_modes.append(HVAC_MODE_OFF) - _modes = list(set(filter(None, _controller_modes))) + hvac_modes = list(set(filter(None, ha_controller_modes))) # default to ["heat"] - return _modes if _modes else [HVAC_MODE_HEAT] + return hvac_modes if hvac_modes else [HVAC_MODE_HEAT] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set operation mode.""" @@ -123,7 +139,10 @@ class KNXClimate(KnxEntity, ClimateEntity): else: if self._device.supports_on_off and not self._device.is_on: await self._device.turn_on() - if self._device.mode.supports_controller_mode: + if ( + self._device.mode is not None + and self._device.mode.supports_controller_mode + ): knx_controller_mode = HVACControllerMode( CONTROLLER_MODES_INV.get(hvac_mode) ) @@ -131,31 +150,33 @@ class KNXClimate(KnxEntity, ClimateEntity): self.async_write_ha_state() @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. Requires SUPPORT_PRESET_MODE. """ - if self._device.mode.supports_operation_mode: + if self._device.mode is not None and self._device.mode.supports_operation_mode: return PRESET_MODES.get(self._device.mode.operation_mode.value, PRESET_AWAY) return None @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. Requires SUPPORT_PRESET_MODE. """ - _presets = [ + if self._device.mode is None: + return None + + presets = [ PRESET_MODES.get(operation_mode.value) for operation_mode in self._device.mode.operation_modes ] - - return list(filter(None, _presets)) + return list(filter(None, presets)) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self._device.mode.supports_operation_mode: + if self._device.mode is not None and self._device.mode.supports_operation_mode: knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self._device.mode.set_operation_mode(knx_operation_mode) self.async_write_ha_state() diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 83ffc2557c2..dfe357ef33c 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -17,11 +17,16 @@ from homeassistant.components.climate.const import ( DOMAIN = "knx" +# Address is used for configuration and services by the same functions so the key has to match +KNX_ADDRESS = "address" + CONF_INVERT = "invert" CONF_STATE_ADDRESS = "state_address" CONF_SYNC_STATE = "sync_state" CONF_RESET_AFTER = "reset_after" +ATTR_COUNTER = "counter" + class ColorTempModes(Enum): """Color temperature modes for config validation.""" @@ -64,5 +69,3 @@ PRESET_MODES = { "Standby": PRESET_AWAY, "Comfort": PRESET_COMFORT, } - -ATTR_COUNTER = "counter" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 4c08612926b..c45d057c3af 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,5 +1,10 @@ """Support for KNX/IP covers.""" -from xknx.devices import Cover as XknxCover +from __future__ import annotations + +from datetime import datetime +from typing import Any, Callable, Iterable + +from xknx.devices import Cover as XknxCover, Device as XknxDevice from homeassistant.components.cover import ( ATTR_POSITION, @@ -16,14 +21,21 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +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 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up cover(s) for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -37,19 +49,20 @@ class KNXCover(KnxEntity, CoverEntity): def __init__(self, device: XknxCover): """Initialize the cover.""" + self._device: XknxCover super().__init__(device) - self._unsubscribe_auto_updater = None + self._unsubscribe_auto_updater: Callable[[], None] | None = None @callback - async def after_update_callback(self, device): + async def after_update_callback(self, device: XknxDevice) -> None: """Call after device was updated.""" self.async_write_ha_state() if self._device.is_traveling(): self.start_auto_updater() @property - def device_class(self): + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" if self._device.device_class in DEVICE_CLASSES: return self._device.device_class @@ -58,7 +71,7 @@ class KNXCover(KnxEntity, CoverEntity): return None @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION if self._device.supports_stop: @@ -73,19 +86,17 @@ class KNXCover(KnxEntity, CoverEntity): return supported_features @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return the current position of the cover. None is unknown, 0 is closed, 100 is fully open. """ # In KNX 0 is open, 100 is closed. - try: - return 100 - self._device.current_position() - except TypeError: - return None + pos = self._device.current_position() + return 100 - pos if pos is not None else None @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" # state shall be "unknown" when xknx travelcalculator is not initialized if self._device.current_position() is None: @@ -93,79 +104,76 @@ class KNXCover(KnxEntity, CoverEntity): return self._device.is_closed() @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._device.is_opening() @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._device.is_closing() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._device.set_down() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._device.set_up() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" knx_position = 100 - kwargs[ATTR_POSITION] await self._device.set_position(knx_position) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._device.stop() self.stop_auto_updater() @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return current tilt position of cover.""" if not self._device.supports_angle: return None - try: - return 100 - self._device.current_angle() - except TypeError: - return None + ang = self._device.current_angle() + return 100 - ang if ang is not None else None - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION] await self._device.set_angle(knx_tilt_position) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" await self._device.set_short_up() - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" await self._device.set_short_down() - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._device.stop() self.stop_auto_updater() - def start_auto_updater(self): + def start_auto_updater(self) -> None: """Start the autoupdater to update Home Assistant while cover is moving.""" if self._unsubscribe_auto_updater is None: self._unsubscribe_auto_updater = async_track_utc_time_change( self.hass, self.auto_updater_hook ) - def stop_auto_updater(self): + def stop_auto_updater(self) -> None: """Stop the autoupdater.""" if self._unsubscribe_auto_updater is not None: self._unsubscribe_auto_updater() self._unsubscribe_auto_updater = None @callback - def auto_updater_hook(self, now): + def auto_updater_hook(self, now: datetime) -> None: """Call for the autoupdater.""" self.async_write_ha_state() if self._device.position_reached(): + self.hass.async_create_task(self._device.auto_stop_if_necessary()) self.stop_auto_updater() - - self.hass.add_job(self._device.auto_stop_if_necessary()) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index 5abc58f82cc..5616a6deb23 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -1,39 +1,44 @@ """Exposures to KNX bus.""" -from typing import Union +from __future__ import annotations + +from typing import Callable from xknx import XKNX from xknx.devices import DateTime, ExposeSensor from homeassistant.const import ( - CONF_ADDRESS, CONF_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, StateType +from .const import KNX_ADDRESS from .schema import ExposeSchema @callback def create_knx_exposure( hass: HomeAssistant, xknx: XKNX, config: ConfigType -) -> Union["KNXExposeSensor", "KNXExposeTime"]: +) -> KNXExposeSensor | KNXExposeTime: """Create exposures from config.""" - address = config[CONF_ADDRESS] + address = config[KNX_ADDRESS] + expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE] attribute = config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE) - entity_id = config.get(CONF_ENTITY_ID) - expose_type = config.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE) default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) - exposure: Union["KNXExposeSensor", "KNXExposeTime"] - if expose_type.lower() in ["time", "date", "datetime"]: + exposure: KNXExposeSensor | KNXExposeTime + if ( + isinstance(expose_type, str) + and expose_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES + ): exposure = KNXExposeTime(xknx, expose_type, address) else: + entity_id = config[CONF_ENTITY_ID] exposure = KNXExposeSensor( hass, xknx, @@ -43,14 +48,22 @@ def create_knx_exposure( default, address, ) - exposure.async_register() return exposure class KNXExposeSensor: """Object to Expose Home Assistant entity to KNX bus.""" - def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address): + def __init__( + self, + hass: HomeAssistant, + xknx: XKNX, + expose_type: int | str, + entity_id: str, + attribute: str | None, + default: StateType, + address: str, + ) -> None: """Initialize of Expose class.""" self.hass = hass self.xknx = xknx @@ -59,17 +72,17 @@ class KNXExposeSensor: self.expose_attribute = attribute self.expose_default = default self.address = address - self.device = None - self._remove_listener = None + self._remove_listener: Callable[[], None] | None = None + self.device: ExposeSensor = self.async_register() @callback - def async_register(self): + def async_register(self) -> ExposeSensor: """Register listener.""" if self.expose_attribute is not None: _name = self.entity_id + "__" + self.expose_attribute else: _name = self.entity_id - self.device = ExposeSensor( + device = ExposeSensor( self.xknx, name=_name, group_address=self.address, @@ -78,6 +91,7 @@ class KNXExposeSensor: self._remove_listener = async_track_state_change_event( self.hass, [self.entity_id], self._async_entity_changed ) + return device @callback def shutdown(self) -> None: @@ -85,10 +99,9 @@ class KNXExposeSensor: if self._remove_listener is not None: self._remove_listener() self._remove_listener = None - if self.device is not None: - self.device.shutdown() + self.device.shutdown() - async def _async_entity_changed(self, event): + async def _async_entity_changed(self, event: Event) -> None: """Handle entity change.""" new_state = event.data.get("new_state") if new_state is None: @@ -96,12 +109,15 @@ class KNXExposeSensor: if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): return + old_state = event.data.get("old_state") + if self.expose_attribute is None: - await self._async_set_knx_value(new_state.state) + if old_state is None or old_state.state != new_state.state: + # don't send same value sequentially + await self._async_set_knx_value(new_state.state) return new_attribute = new_state.attributes.get(self.expose_attribute) - old_state = event.data.get("old_state") if old_state is not None: old_attribute = old_state.attributes.get(self.expose_attribute) @@ -110,8 +126,9 @@ class KNXExposeSensor: return await self._async_set_knx_value(new_attribute) - async def _async_set_knx_value(self, value): + async def _async_set_knx_value(self, value: StateType) -> None: """Set new value on xknx ExposeSensor.""" + assert self.device is not None if value is None: if self.expose_default is None: return @@ -129,17 +146,17 @@ class KNXExposeSensor: class KNXExposeTime: """Object to Expose Time/Date object to KNX bus.""" - def __init__(self, xknx: XKNX, expose_type: str, address: str): + def __init__(self, xknx: XKNX, expose_type: str, address: str) -> None: """Initialize of Expose class.""" self.xknx = xknx self.expose_type = expose_type self.address = address - self.device = None + self.device: DateTime = self.async_register() @callback - def async_register(self): + def async_register(self) -> DateTime: """Register listener.""" - self.device = DateTime( + return DateTime( self.xknx, name=self.expose_type.capitalize(), broadcast_type=self.expose_type.upper(), @@ -148,7 +165,6 @@ class KNXExposeTime: ) @callback - def shutdown(self): + def shutdown(self) -> None: """Prepare for deletion.""" - if self.device is not None: - self.device.shutdown() + self.device.shutdown() diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 51a94bc06e3..827ec83a8e1 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -1,5 +1,5 @@ """Factory function to initialize KNX devices from config.""" -from typing import Optional, Tuple +from __future__ import annotations from xknx import XKNX from xknx.devices import ( @@ -17,10 +17,10 @@ from xknx.devices import ( Weather as XknxWeather, ) -from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE from homeassistant.helpers.typing import ConfigType -from .const import ColorTempModes, SupportedPlatforms +from .const import KNX_ADDRESS, ColorTempModes, SupportedPlatforms from .schema import ( BinarySensorSchema, ClimateSchema, @@ -95,11 +95,11 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: def _create_light_color( color: str, config: ConfigType -) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: +) -> tuple[str | None, str | None, str | None, str | None]: """Load color configuration from configuration structure.""" if "individual_colors" in config and color in config["individual_colors"]: sub_config = config["individual_colors"][color] - group_address_switch = sub_config.get(CONF_ADDRESS) + group_address_switch = sub_config.get(KNX_ADDRESS) group_address_switch_state = sub_config.get(LightSchema.CONF_STATE_ADDRESS) group_address_brightness = sub_config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS) group_address_brightness_state = sub_config.get( @@ -160,7 +160,7 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: return XknxLight( knx_module, name=config[CONF_NAME], - group_address_switch=config.get(CONF_ADDRESS), + group_address_switch=config.get(KNX_ADDRESS), group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS), group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS), group_address_brightness_state=config.get( @@ -264,9 +264,9 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), mode=climate_mode, on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], - create_temperature_sensors=config.get( + create_temperature_sensors=config[ ClimateSchema.CONF_CREATE_TEMPERATURE_SENSORS - ), + ], ) @@ -275,9 +275,9 @@ def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch: return XknxSwitch( knx_module, name=config[CONF_NAME], - group_address=config[CONF_ADDRESS], + group_address=config[KNX_ADDRESS], group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), - invert=config.get(SwitchSchema.CONF_INVERT), + invert=config[SwitchSchema.CONF_INVERT], ) @@ -298,7 +298,7 @@ def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification: return XknxNotification( knx_module, name=config[CONF_NAME], - group_address=config[CONF_ADDRESS], + group_address=config[KNX_ADDRESS], ) @@ -307,7 +307,7 @@ def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene: return XknxScene( knx_module, name=config[CONF_NAME], - group_address=config[CONF_ADDRESS], + group_address=config[KNX_ADDRESS], scene_number=config[SceneSchema.CONF_SCENE_NUMBER], ) @@ -320,7 +320,7 @@ def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySen knx_module, name=device_name, group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], - invert=config.get(BinarySensorSchema.CONF_INVERT), + invert=config[BinarySensorSchema.CONF_INVERT], sync_state=config[BinarySensorSchema.CONF_SYNC_STATE], device_class=config.get(CONF_DEVICE_CLASS), ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE], @@ -372,7 +372,7 @@ def _create_fan(knx_module: XKNX, config: ConfigType) -> XknxFan: fan = XknxFan( knx_module, name=config[CONF_NAME], - group_address_speed=config.get(CONF_ADDRESS), + group_address_speed=config.get(KNX_ADDRESS), group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS), group_address_oscillation=config.get(FanSchema.CONF_OSCILLATION_ADDRESS), group_address_oscillation_state=config.get( diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 43d1cd7d6f2..38680e15bf8 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -1,11 +1,15 @@ """Support for KNX/IP fans.""" +from __future__ import annotations + import math -from typing import Any, Optional +from typing import Any, Callable, Iterable from xknx.devices import Fan as XknxFan -from xknx.devices.fan import FanSpeedMode from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -18,7 +22,12 @@ from .knx_entity import KnxEntity DEFAULT_PERCENTAGE = 50 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up fans for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -30,18 +39,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXFan(KnxEntity, FanEntity): """Representation of a KNX fan.""" - def __init__(self, device: XknxFan): + def __init__(self, device: XknxFan) -> None: """Initialize of KNX fan.""" + self._device: XknxFan super().__init__(device) - if self._device.mode == FanSpeedMode.STEP: + self._step_range: tuple[int, int] | None = None + if device.max_step: + # FanSpeedMode.STEP: self._step_range = (1, device.max_step) - else: - self._step_range = None async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" - if self._device.mode == FanSpeedMode.STEP: + if self._step_range: step = math.ceil(percentage_to_ranged_value(self._step_range, percentage)) await self._device.set_speed(step) else: @@ -58,12 +68,12 @@ class KNXFan(KnxEntity, FanEntity): return flags @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the current speed as a percentage.""" if self._device.current_speed is None: return None - if self._device.mode == FanSpeedMode.STEP: + if self._step_range: return ranged_value_to_percentage( self._step_range, self._device.current_speed ) @@ -78,10 +88,10 @@ class KNXFan(KnxEntity, FanEntity): async def async_turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the fan.""" if percentage is None: @@ -98,6 +108,6 @@ class KNXFan(KnxEntity, FanEntity): await self._device.set_oscillation(oscillating) @property - def oscillating(self): + def oscillating(self) -> bool | None: """Return whether or not the fan is currently oscillating.""" return self._device.current_oscillation diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index f4597ad230e..670f1ddf44d 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -1,38 +1,49 @@ """Base class for KNX devices.""" +from __future__ import annotations + +from typing import cast + from xknx.devices import Climate as XknxClimate, Device as XknxDevice from homeassistant.helpers.entity import Entity +from . import KNXModule from .const import DOMAIN class KnxEntity(Entity): """Representation of a KNX entity.""" - def __init__(self, device: XknxDevice): + def __init__(self, device: XknxDevice) -> None: """Set up device.""" self._device = device @property - def name(self): + def name(self) -> str: """Return the name of the KNX device.""" return self._device.name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" - return self.hass.data[DOMAIN].connected + knx_module = cast(KNXModule, self.hass.data[DOMAIN]) + return knx_module.connected @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed within KNX.""" return False - async def async_update(self): + @property + def unique_id(self) -> str | None: + """Return the unique id of the device.""" + return self._device.unique_id + + async def async_update(self) -> None: """Request a state update from KNX bus.""" await self._device.sync() - async def after_update_callback(self, device: XknxDevice): + async def after_update_callback(self, device: XknxDevice) -> None: """Call after device was updated.""" self.async_write_ha_state() diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 50d067bf29a..0eb62433734 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,4 +1,8 @@ """Support for KNX/IP lights.""" +from __future__ import annotations + +from typing import Any, Callable, Iterable + from xknx.devices import Light as XknxLight from homeassistant.components.light import ( @@ -12,17 +16,26 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) +from homeassistant.core import HomeAssistant +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 .knx_entity import KnxEntity +from .schema import LightSchema DEFAULT_COLOR = (0.0, 0.0) DEFAULT_BRIGHTNESS = 255 DEFAULT_WHITE_VALUE = 255 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up lights for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -34,12 +47,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXLight(KnxEntity, LightEntity): """Representation of a KNX light.""" - def __init__(self, device: XknxLight): + def __init__(self, device: XknxLight) -> None: """Initialize of KNX light.""" + self._device: XknxLight super().__init__(device) - self._min_kelvin = device.min_kelvin - self._max_kelvin = device.max_kelvin + 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( self._max_kelvin ) @@ -48,7 +62,7 @@ class KNXLight(KnxEntity, LightEntity): ) @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" if self._device.supports_brightness: return self._device.current_brightness @@ -58,31 +72,31 @@ class KNXLight(KnxEntity, LightEntity): return None @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the HS color value.""" - rgb = None + rgb: tuple[int, int, int] | None = None if self._device.supports_rgbw or self._device.supports_color: rgb, _ = self._device.current_color return color_util.color_RGB_to_hs(*rgb) if rgb else None @property - def _hsv_color(self): + def _hsv_color(self) -> tuple[float, float, float] | None: """Return the HSV color value.""" - rgb = None + rgb: tuple[int, int, int] | None = None if self._device.supports_rgbw or self._device.supports_color: rgb, _ = self._device.current_color return color_util.color_RGB_to_hsv(*rgb) if rgb else None @property - def white_value(self): + def white_value(self) -> int | None: """Return the white value.""" - white = None + white: int | None = None if self._device.supports_rgbw: _, white = self._device.current_color return white @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature in mireds.""" if self._device.supports_color_temperature: kelvin = self._device.current_color_temperature @@ -101,32 +115,32 @@ class KNXLight(KnxEntity, LightEntity): return None @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color temp this light supports in mireds.""" return self._min_mireds @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color temp this light supports in mireds.""" return self._max_mireds @property - def effect_list(self): + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return None @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" return None @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" - return self._device.state + return bool(self._device.state) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" flags = 0 if self._device.supports_brightness: @@ -142,7 +156,7 @@ class KNXLight(KnxEntity, LightEntity): flags |= SUPPORT_COLOR_TEMP return flags - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) @@ -183,7 +197,8 @@ class KNXLight(KnxEntity, LightEntity): hs_color = DEFAULT_COLOR if white_value is None and self._device.supports_rgbw: white_value = DEFAULT_WHITE_VALUE - rgb = color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255) + hsv_color = hs_color + (brightness * 100 / 255,) + rgb = color_util.color_hsv_to_RGB(*hsv_color) await self._device.set_color(rgb, white_value) if update_color_temp: @@ -200,6 +215,6 @@ class KNXLight(KnxEntity, LightEntity): ) await self._device.set_tunable_white(relative_ct) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.set_off() diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 7ca7657d0ff..f15e909755c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.17.1"], + "requirements": ["xknx==0.17.5"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 7210795bd71..62ba1109526 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,14 +1,22 @@ """Support for KNX/IP notification services.""" -from typing import List +from __future__ import annotations + +from typing import Any from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import BaseNotificationService +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN -async def async_get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> KNXNotificationService | None: """Get the KNX notification service.""" notification_devices = [] for device in hass.data[DOMAIN].xknx.devices: @@ -22,31 +30,31 @@ async def async_get_service(hass, config, discovery_info=None): class KNXNotificationService(BaseNotificationService): """Implement demo notification service.""" - def __init__(self, devices: List[XknxNotification]): + def __init__(self, devices: list[XknxNotification]) -> None: """Initialize the service.""" self.devices = devices @property - def targets(self): + def targets(self) -> dict[str, str]: """Return a dictionary of registered targets.""" ret = {} for device in self.devices: ret[device.name] = device.name return ret - async def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a notification to knx bus.""" if "target" in kwargs: await self._async_send_to_device(message, kwargs["target"]) else: await self._async_send_to_all_devices(message) - async def _async_send_to_all_devices(self, message): + async def _async_send_to_all_devices(self, message: str) -> None: """Send a notification to knx bus to all connected devices.""" for device in self.devices: await device.set(message) - async def _async_send_to_device(self, message, names): + async def _async_send_to_device(self, message: str, names: str) -> None: """Send a notification to knx bus to device with given names.""" for device in self.devices: if device.name in names: diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 6c76fdbd199..ff08cdf411c 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,15 +1,25 @@ """Support for KNX scenes.""" -from typing import Any +from __future__ import annotations + +from typing import Any, Callable, Iterable from xknx.devices import Scene as XknxScene from homeassistant.components.scene import Scene +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .knx_entity import KnxEntity -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the scenes for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -21,8 +31,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXScene(KnxEntity, Scene): """Representation of a KNX scene.""" - def __init__(self, device: XknxScene): + def __init__(self, device: XknxScene) -> None: """Init KNX scene.""" + self._device: XknxScene super().__init__(device) async def async_activate(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 08a8c62adc4..fb4b29fbd70 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -5,7 +5,6 @@ from xknx.io import DEFAULT_MCAST_PORT from xknx.telegram.address import GroupAddress, IndividualAddress from homeassistant.const import ( - CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_HOST, @@ -21,6 +20,7 @@ from .const import ( CONF_STATE_ADDRESS, CONF_SYNC_STATE, CONTROLLER_MODES, + KNX_ADDRESS, PRESET_MODES, ColorTempModes, ) @@ -30,13 +30,14 @@ from .const import ( ################## ga_validator = vol.Any( - cv.matches_regex(GroupAddress.ADDRESS_RE), + 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')", ) +ga_list_validator = vol.All(cv.ensure_list, [ga_validator]) ia_validator = vol.Any( - cv.matches_regex(IndividualAddress.ADDRESS_RE), + cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern), vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), msg="value does not match pattern for KNX individual address '..' (eg.'1.1.100')", ) @@ -47,6 +48,8 @@ sync_state_validator = vol.Any( cv.matches_regex(r"^(init|expire|every)( \d*)?$"), ) +sensor_type_validator = vol.Any(int, str) + ############## # CONNECTION @@ -57,12 +60,14 @@ class ConnectionSchema: """Voluptuous schema for KNX connection.""" CONF_KNX_LOCAL_IP = "local_ip" + CONF_KNX_ROUTE_BACK = "route_back" TUNNELING_SCHEMA = vol.Schema( { vol.Optional(CONF_PORT, default=DEFAULT_MCAST_PORT): cv.port, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_KNX_LOCAL_IP): cv.string, + vol.Optional(CONF_KNX_ROUTE_BACK, default=False): cv.boolean, } ) @@ -94,12 +99,12 @@ class BinarySensorSchema: vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean, - vol.Required(CONF_STATE_ADDRESS): ga_validator, + vol.Optional(CONF_INVERT, default=False): cv.boolean, + vol.Required(CONF_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_CONTEXT_TIMEOUT): vol.All( vol.Coerce(float), vol.Range(min=0, max=10) ), vol.Optional(CONF_DEVICE_CLASS): cv.string, - vol.Optional(CONF_INVERT): cv.boolean, vol.Optional(CONF_RESET_AFTER): cv.positive_float, } ), @@ -165,27 +170,27 @@ class ClimateSchema: vol.Optional( CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP ): vol.All(float, vol.Range(min=0, max=2)), - vol.Required(CONF_TEMPERATURE_ADDRESS): ga_validator, - vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): ga_validator, - vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): ga_validator, - vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_validator, - vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_validator, - vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): ga_validator, - vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_HEAT_COOL_ADDRESS): ga_validator, - vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): ga_validator, + vol.Required(CONF_TEMPERATURE_ADDRESS): ga_list_validator, + vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): ga_list_validator, + vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): ga_list_validator, + vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_OPERATION_MODE_ADDRESS): ga_list_validator, + vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): ga_list_validator, + vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): ga_list_validator, + vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_HEAT_COOL_ADDRESS): ga_list_validator, + vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): ga_list_validator, vol.Optional( CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS - ): ga_validator, - vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): ga_validator, - vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): ga_validator, - vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): ga_validator, - vol.Optional(CONF_ON_OFF_ADDRESS): ga_validator, - vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_validator, + ): ga_list_validator, + vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): ga_list_validator, + vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): ga_list_validator, + vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): ga_list_validator, + vol.Optional(CONF_ON_OFF_ADDRESS): ga_list_validator, + vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_list_validator, vol.Optional( CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT ): cv.boolean, @@ -226,13 +231,13 @@ class CoverSchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_validator, - vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_validator, - vol.Optional(CONF_STOP_ADDRESS): ga_validator, - vol.Optional(CONF_POSITION_ADDRESS): ga_validator, - vol.Optional(CONF_POSITION_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_ANGLE_ADDRESS): ga_validator, - vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_validator, + 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, @@ -252,16 +257,30 @@ class ExposeSchema: CONF_KNX_EXPOSE_TYPE = CONF_TYPE CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" CONF_KNX_EXPOSE_DEFAULT = "default" + EXPOSE_TIME_TYPES = [ + "time", + "date", + "datetime", + ] - SCHEMA = vol.Schema( + EXPOSE_TIME_SCHEMA = vol.Schema( { - vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(int, float, str), - vol.Required(CONF_ADDRESS): ga_validator, - vol.Optional(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_KNX_EXPOSE_TYPE): vol.All( + cv.string, str.lower, vol.In(EXPOSE_TIME_TYPES) + ), + vol.Required(KNX_ADDRESS): ga_validator, + } + ) + EXPOSE_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_KNX_EXPOSE_TYPE): sensor_type_validator, + vol.Required(KNX_ADDRESS): ga_validator, + vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, } ) + SCHEMA = vol.Any(EXPOSE_TIME_SCHEMA, EXPOSE_SENSOR_SCHEMA) class FanSchema: @@ -277,10 +296,10 @@ class FanSchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): ga_validator, - vol.Optional(CONF_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_OSCILLATION_ADDRESS): ga_validator, - vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator, + vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MAX_STEP): cv.byte, } ) @@ -315,10 +334,10 @@ class LightSchema: COLOR_SCHEMA = vol.Schema( { - vol.Optional(CONF_ADDRESS): ga_validator, - vol.Optional(CONF_STATE_ADDRESS): ga_validator, - vol.Required(CONF_BRIGHTNESS_ADDRESS): ga_validator, - vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_validator, + vol.Optional(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Required(CONF_BRIGHTNESS_ADDRESS): ga_list_validator, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator, } ) @@ -326,25 +345,25 @@ class LightSchema: vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ADDRESS): ga_validator, - vol.Optional(CONF_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_validator, - vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_validator, + vol.Optional(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_list_validator, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): ga_list_validator, vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA, vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA, vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA, vol.Optional(CONF_WHITE): COLOR_SCHEMA, }, - vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_validator, - vol.Optional(CONF_COLOR_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_COLOR_TEMP_ADDRESS): ga_validator, - vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): ga_validator, + vol.Exclusive(CONF_COLOR_ADDRESS, "color"): ga_list_validator, + vol.Optional(CONF_COLOR_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_COLOR_TEMP_ADDRESS): ga_list_validator, + vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): ga_list_validator, vol.Optional( CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE ): vol.All(vol.Upper, cv.enum(ColorTempModes)), - vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_validator, - vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_validator, + vol.Exclusive(CONF_RGBW_ADDRESS, "color"): ga_list_validator, + vol.Optional(CONF_RGBW_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), @@ -358,16 +377,16 @@ class LightSchema: vol.Schema( { vol.Required(CONF_INDIVIDUAL_COLORS): { - vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object}, - vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object}, - vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_RED): {vol.Required(KNX_ADDRESS): object}, + vol.Required(CONF_GREEN): {vol.Required(KNX_ADDRESS): object}, + vol.Required(CONF_BLUE): {vol.Required(KNX_ADDRESS): object}, }, }, extra=vol.ALLOW_EXTRA, ), vol.Schema( { - vol.Required(CONF_ADDRESS): object, + vol.Required(KNX_ADDRESS): object, }, extra=vol.ALLOW_EXTRA, ), @@ -383,7 +402,7 @@ class NotifySchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): ga_validator, } ) @@ -397,7 +416,7 @@ class SceneSchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): ga_validator, + vol.Required(KNX_ADDRESS): ga_list_validator, vol.Required(CONF_SCENE_NUMBER): cv.positive_int, } ) @@ -416,8 +435,8 @@ class SensorSchema: vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean, - vol.Required(CONF_STATE_ADDRESS): ga_validator, - vol.Required(CONF_TYPE): vol.Any(int, float, str), + vol.Required(CONF_TYPE): sensor_type_validator, + vol.Required(CONF_STATE_ADDRESS): ga_list_validator, } ) @@ -432,9 +451,9 @@ class SwitchSchema: SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): ga_validator, - vol.Optional(CONF_STATE_ADDRESS): ga_validator, - vol.Optional(CONF_INVERT): cv.boolean, + vol.Optional(CONF_INVERT, default=False): cv.boolean, + vol.Required(KNX_ADDRESS): ga_list_validator, + vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, } ) @@ -465,18 +484,18 @@ class WeatherSchema: vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, vol.Optional(CONF_KNX_CREATE_SENSORS, default=False): cv.boolean, - vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_validator, - vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_validator, + vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, + vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, } ) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 2409d7a6425..f14cf7e5b29 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,14 +1,25 @@ """Support for KNX/IP sensors.""" +from __future__ import annotations + +from typing import Callable, Iterable + from xknx.devices import Sensor as XknxSensor -from homeassistant.components.sensor import DEVICE_CLASSES +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 .const import DOMAIN from .knx_entity import KnxEntity -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up sensor(s) for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -17,25 +28,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class KNXSensor(KnxEntity, Entity): +class KNXSensor(KnxEntity, SensorEntity): """Representation of a KNX sensor.""" - def __init__(self, device: XknxSensor): + def __init__(self, device: XknxSensor) -> None: """Initialize of a KNX sensor.""" + self._device: XknxSensor super().__init__(device) @property - def state(self): + def state(self) -> StateType: """Return the state of the sensor.""" return self._device.resolve_state() @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._device.unit_of_measurement() @property - def device_class(self): + def device_class(self) -> str | None: """Return the device class of the sensor.""" device_class = self._device.ha_device_class() if device_class in DEVICE_CLASSES: diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 3fae7dfce0e..c13abb23d94 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -4,11 +4,11 @@ send: fields: address: name: "Group address" - description: "Group address(es) to write to." + description: "Group address(es) to write to. Lists will send to multiple group addresses successively." required: true example: "1/1/0" selector: - text: + object: payload: name: "Payload" description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." @@ -33,21 +33,21 @@ read: required: true example: "1/1/0" selector: - text: + object: event_register: name: "Register knx_event" - description: "Add or remove single group address to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." + description: "Add or remove group addresses to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." fields: address: name: "Group address" - description: "Group address that shall be added or removed." + description: "Group address(es) that shall be added or removed. Lists are allowed." required: true example: "1/1/0" selector: - text: + object: remove: name: "Remove event registration" - description: "Optional. If `True` the group address will be removed." + description: "Optional. If `True` the group address(es) will be removed." default: false selector: boolean: diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index ae3048e2d23..82fe2f40be3 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,13 +1,25 @@ """Support for KNX/IP switches.""" +from __future__ import annotations + +from typing import Any, Callable, Iterable + from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN +from .const import DOMAIN from .knx_entity import KnxEntity -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up switch(es) for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: @@ -19,19 +31,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXSwitch(KnxEntity, SwitchEntity): """Representation of a KNX switch.""" - def __init__(self, device: XknxSwitch): + def __init__(self, device: XknxSwitch) -> None: """Initialize of KNX switch.""" + self._device: XknxSwitch super().__init__(device) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self._device.state + return bool(self._device.state) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._device.set_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._device.set_off() diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 031af9f5af0..cc2f3c0a09c 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,16 +1,27 @@ """Support for KNX/IP weather station.""" +from __future__ import annotations + +from typing import Callable, Iterable from xknx.devices import Weather as XknxWeather from homeassistant.components.weather import WeatherEntity from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .knx_entity import KnxEntity -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the scenes for KNX platform.""" +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: Callable[[Iterable[Entity]], None], + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up weather entities for KNX platform.""" entities = [] for device in hass.data[DOMAIN].xknx.devices: if isinstance(device, XknxWeather): @@ -21,22 +32,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" - def __init__(self, device: XknxWeather): + def __init__(self, device: XknxWeather) -> None: """Initialize of a KNX sensor.""" + self._device: XknxWeather super().__init__(device) @property - def temperature(self): + def temperature(self) -> float | None: """Return current temperature.""" return self._device.temperature @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return temperature unit.""" return TEMP_CELSIUS @property - def pressure(self): + def pressure(self) -> float | None: """Return current air pressure.""" # KNX returns pA - HA requires hPa return ( @@ -46,22 +58,22 @@ class KNXWeather(KnxEntity, WeatherEntity): ) @property - def condition(self): + def condition(self) -> str: """Return current weather condition.""" return self._device.ha_current_state().value @property - def humidity(self): + def humidity(self) -> float | None: """Return current humidity.""" return self._device.humidity @property - def wind_bearing(self): + def wind_bearing(self) -> int | None: """Return current wind bearing in degrees.""" return self._device.wind_bearing @property - def wind_speed(self): + def wind_speed(self) -> float | None: """Return current wind speed in km/h.""" # KNX only supports wind speed in m/s return ( diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 4dcb25b3ea9..ea867e8c407 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -72,9 +72,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DATA_REMOVE_LISTENER: remove_stop_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -85,8 +85,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index bdbac4ab762..a7e87a6ae27 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" +import asyncio import logging from homeassistant.components.media_player import BrowseError, BrowseMedia @@ -58,123 +59,20 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" -async def build_item_response(media_library, payload): +async def build_item_response(media_library, payload, get_thumbnail_url=None): """Create response payload for the provided media query.""" search_id = payload["search_id"] search_type = payload["search_type"] - thumbnail = None - title = None - media = None - - properties = ["thumbnail"] - if search_type == MEDIA_TYPE_ALBUM: - if search_id: - album = await media_library.get_album_details( - album_id=int(search_id), properties=properties - ) - thumbnail = media_library.thumbnail_url( - album["albumdetails"].get("thumbnail") - ) - title = album["albumdetails"]["label"] - media = await media_library.get_songs( - album_id=int(search_id), - properties=[ - "albumid", - "artist", - "duration", - "album", - "thumbnail", - "track", - ], - ) - media = media.get("songs") - else: - media = await media_library.get_albums(properties=properties) - media = media.get("albums") - title = "Albums" - - elif search_type == MEDIA_TYPE_ARTIST: - if search_id: - media = await media_library.get_albums( - artist_id=int(search_id), properties=properties - ) - media = media.get("albums") - artist = await media_library.get_artist_details( - artist_id=int(search_id), properties=properties - ) - thumbnail = media_library.thumbnail_url( - artist["artistdetails"].get("thumbnail") - ) - title = artist["artistdetails"]["label"] - else: - media = await media_library.get_artists(properties) - media = media.get("artists") - title = "Artists" - - elif search_type == "library_music": - library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} - media = [{"label": name, "type": type_} for type_, name in library.items()] - title = "Music Library" - - elif search_type == MEDIA_TYPE_MOVIE: - media = await media_library.get_movies(properties) - media = media.get("movies") - title = "Movies" - - elif search_type == MEDIA_TYPE_TVSHOW: - if search_id: - media = await media_library.get_seasons( - tv_show_id=int(search_id), - properties=["thumbnail", "season", "tvshowid"], - ) - media = media.get("seasons") - tvshow = await media_library.get_tv_show_details( - tv_show_id=int(search_id), properties=properties - ) - thumbnail = media_library.thumbnail_url( - tvshow["tvshowdetails"].get("thumbnail") - ) - title = tvshow["tvshowdetails"]["label"] - else: - media = await media_library.get_tv_shows(properties) - media = media.get("tvshows") - title = "TV Shows" - - elif search_type == MEDIA_TYPE_SEASON: - tv_show_id, season_id = search_id.split("/", 1) - media = await media_library.get_episodes( - tv_show_id=int(tv_show_id), - season_id=int(season_id), - properties=["thumbnail", "tvshowid", "seasonid"], - ) - media = media.get("episodes") - if media: - season = await media_library.get_season_details( - season_id=int(media[0]["seasonid"]), properties=properties - ) - thumbnail = media_library.thumbnail_url( - season["seasondetails"].get("thumbnail") - ) - title = season["seasondetails"]["label"] - - elif search_type == MEDIA_TYPE_CHANNEL: - media = await media_library.get_channels( - channel_group_id="alltv", - properties=["thumbnail", "channeltype", "channel", "broadcastnow"], - ) - media = media.get("channels") - title = "Channels" + _, title, media = await get_media_info(media_library, search_id, search_type) + thumbnail = await get_thumbnail_url(search_type, search_id) if media is None: return None - children = [] - for item in media: - try: - children.append(item_payload(item, media_library)) - except UnknownMediaType: - pass + children = await asyncio.gather( + *[item_payload(item, get_thumbnail_url) for item in media] + ) if search_type in (MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE) and search_id == "": children.sort(key=lambda x: x.title.replace("The ", "", 1), reverse=False) @@ -200,16 +98,13 @@ async def build_item_response(media_library, payload): return response -def item_payload(item, media_library): +async def item_payload(item, get_thumbnail_url=None): """ Create response payload for a single media item. Used by async_browse_media. """ title = item["label"] - thumbnail = item.get("thumbnail") - if thumbnail: - thumbnail = media_library.thumbnail_url(thumbnail) media_class = None @@ -273,6 +168,12 @@ def item_payload(item, media_library): _LOGGER.debug("Unknown media type received: %s", media_content_type) raise UnknownMediaType from err + thumbnail = item.get("thumbnail") + if thumbnail is not None and get_thumbnail_url is not None: + thumbnail = await get_thumbnail_url( + media_content_type, media_content_id, thumbnail_url=thumbnail + ) + return BrowseMedia( title=title, media_class=media_class, @@ -284,7 +185,7 @@ def item_payload(item, media_library): ) -def library_payload(media_library): +async def library_payload(): """ Create response payload to describe contents of a specific library. @@ -306,12 +207,137 @@ def library_payload(media_library): MEDIA_TYPE_TVSHOW: "TV shows", MEDIA_TYPE_CHANNEL: "Channels", } - for item in [{"label": name, "type": type_} for type_, name in library.items()]: - library_info.children.append( + + library_info.children = await asyncio.gather( + *[ item_payload( - {"label": item["label"], "type": item["type"], "uri": item["type"]}, - media_library, + { + "label": item["label"], + "type": item["type"], + "uri": item["type"], + }, ) - ) + for item in [ + {"label": name, "type": type_} for type_, name in library.items() + ] + ] + ) return library_info + + +async def get_media_info(media_library, search_id, search_type): + """Fetch media/album.""" + thumbnail = None + title = None + media = None + + properties = ["thumbnail"] + if search_type == MEDIA_TYPE_ALBUM: + if search_id: + album = await media_library.get_album_details( + album_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + album["albumdetails"].get("thumbnail") + ) + title = album["albumdetails"]["label"] + media = await media_library.get_songs( + album_id=int(search_id), + properties=[ + "albumid", + "artist", + "duration", + "album", + "thumbnail", + "track", + ], + ) + media = media.get("songs") + else: + media = await media_library.get_albums(properties=properties) + media = media.get("albums") + title = "Albums" + + elif search_type == MEDIA_TYPE_ARTIST: + if search_id: + media = await media_library.get_albums( + artist_id=int(search_id), properties=properties + ) + media = media.get("albums") + artist = await media_library.get_artist_details( + artist_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + artist["artistdetails"].get("thumbnail") + ) + title = artist["artistdetails"]["label"] + else: + media = await media_library.get_artists(properties) + media = media.get("artists") + title = "Artists" + + elif search_type == "library_music": + library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} + media = [{"label": name, "type": type_} for type_, name in library.items()] + title = "Music Library" + + elif search_type == MEDIA_TYPE_MOVIE: + if search_id: + movie = await media_library.get_movie_details( + movie_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + movie["moviedetails"].get("thumbnail") + ) + title = movie["moviedetails"]["label"] + else: + media = await media_library.get_movies(properties) + media = media.get("movies") + title = "Movies" + + elif search_type == MEDIA_TYPE_TVSHOW: + if search_id: + media = await media_library.get_seasons( + tv_show_id=int(search_id), + properties=["thumbnail", "season", "tvshowid"], + ) + media = media.get("seasons") + tvshow = await media_library.get_tv_show_details( + tv_show_id=int(search_id), properties=properties + ) + thumbnail = media_library.thumbnail_url( + tvshow["tvshowdetails"].get("thumbnail") + ) + title = tvshow["tvshowdetails"]["label"] + else: + media = await media_library.get_tv_shows(properties) + media = media.get("tvshows") + title = "TV Shows" + + elif search_type == MEDIA_TYPE_SEASON: + tv_show_id, season_id = search_id.split("/", 1) + media = await media_library.get_episodes( + tv_show_id=int(tv_show_id), + season_id=int(season_id), + properties=["thumbnail", "tvshowid", "seasonid"], + ) + media = media.get("episodes") + if media: + season = await media_library.get_season_details( + season_id=int(media[0]["seasonid"]), properties=properties + ) + thumbnail = media_library.thumbnail_url( + season["seasondetails"].get("thumbnail") + ) + title = season["seasondetails"]["label"] + + elif search_type == MEDIA_TYPE_CHANNEL: + media = await media_library.get_channels( + channel_group_id="alltv", + properties=["thumbnail", "channeltype", "channel", "broadcastnow"], + ) + media = media.get("channels") + title = "Channels" + + return thumbnail, title, media diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 69460a57570..0f5509a4e66 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -24,8 +24,8 @@ from .const import ( DEFAULT_SSL, DEFAULT_TIMEOUT, DEFAULT_WS_PORT, + DOMAIN, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index 314f73a927e..b8653290c0d 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Kodi.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -29,7 +29,7 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Kodi devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -61,8 +61,13 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: @callback def _attach_trigger( - hass: HomeAssistant, config: ConfigType, action: AutomationActionType, event_type + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + event_type, + automation_info: dict, ): + trigger_id = automation_info.get("trigger_id") if automation_info else None job = HassJob(action) @callback @@ -70,7 +75,7 @@ def _attach_trigger( if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: hass.async_run_hass_job( job, - {"trigger": {**config, "description": event_type}}, + {"trigger": {**config, "description": event_type, "id": trigger_id}}, event.context, ) @@ -84,12 +89,10 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] == "turn_on": - return _attach_trigger(hass, config, action, EVENT_TURN_ON) + return _attach_trigger(hass, config, action, EVENT_TURN_ON, automation_info) if config[CONF_TYPE] == "turn_off": - return _attach_trigger(hass, config, action, EVENT_TURN_OFF) + return _attach_trigger(hass, config, action, EVENT_TURN_OFF, automation_info) return lambda: None diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 24d3393d7c3..63282ed1a9a 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -3,7 +3,7 @@ "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ - "pykodi==0.2.1" + "pykodi==0.2.3" ], "codeowners": [ "@OnFreund", diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 6b849cb711b..72197f6b8e2 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -3,8 +3,10 @@ from datetime import timedelta from functools import wraps import logging import re +import urllib.parse import jsonrpc_base +from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError import voluptuous as vol @@ -61,9 +63,10 @@ from homeassistant.helpers import ( entity_platform, ) from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.network import is_internal_request import homeassistant.util.dt as dt_util -from .browse_media import build_item_response, library_payload +from .browse_media import build_item_response, get_media_info, library_payload from .const import ( CONF_WS_PORT, DATA_CONNECTION, @@ -871,16 +874,50 @@ class KodiEntity(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + is_internal = is_internal_request(self.hass) + + async def _get_thumbnail_url( + media_content_type, + media_content_id, + media_image_id=None, + thumbnail_url=None, + ): + if is_internal: + return self._kodi.thumbnail_url(thumbnail_url) + + return self.get_browse_image_url( + media_content_type, + urllib.parse.quote_plus(media_content_id), + media_image_id, + ) + if media_content_type in [None, "library"]: - return await self.hass.async_add_executor_job(library_payload, self._kodi) + return await library_payload() payload = { "search_type": media_content_type, "search_id": media_content_id, } - response = await build_item_response(self._kodi, payload) + + response = await build_item_response(self._kodi, payload, _get_thumbnail_url) if response is None: raise BrowseError( f"Media not found: {media_content_type} / {media_content_id}" ) return response + + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Get media image from kodi server.""" + try: + image_url, _, _ = await get_media_info( + self._kodi, media_content_id, media_content_type + ) + except (ProtocolError, TransportError): + return (None, None) + + if image_url: + return await self._async_fetch_image(image_url) + + return (None, None) diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index 1d229e5a428..15fd212fdbd 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_uuid": "Die Kodi-Instanz hat keine eindeutige ID. Dies ist h\u00f6chstwahrscheinlich auf eine alte Kodi-Version (17.x oder niedriger) zur\u00fcckzuf\u00fchren. Du kannst die Integration manuell konfigurieren oder auf eine neuere Kodi-Version aktualisieren.", "unknown": "Unerwarteter Fehler" }, "error": { diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json index 3b2d79a34a7..64dbfac0c8b 100644 --- a/homeassistant/components/kodi/translations/hu.json +++ b/homeassistant/components/kodi/translations/hu.json @@ -1,7 +1,41 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Add meg a Kodi felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t. Ezek megtal\u00e1lhat\u00f3k a Rendszer/Be\u00e1ll\u00edt\u00e1sok/H\u00e1l\u00f3zat/Szolg\u00e1ltat\u00e1sok r\u00e9szben." + }, + "discovery_confirm": { + "description": "Szeretn\u00e9d hozz\u00e1adni a Kodi (`{name}`)-t a Home Assistant-hoz?", + "title": "Felfedezett Kodi" + }, + "user": { + "data": { + "host": "Hoszt", + "port": "Port", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata" + } + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/id.json b/homeassistant/components/kodi/translations/id.json new file mode 100644 index 00000000000..1a81ab72fab --- /dev/null +++ b/homeassistant/components/kodi/translations/id.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "no_uuid": "Instans Kodi tidak memiliki ID yang unik. Ini kemungkinan besar karena versi Kodi lama (17.x atau sebelumnya). Anda dapat mengonfigurasi integrasi secara manual atau meningkatkan ke versi Kodi yang lebih baru.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan nama pengguna dan kata sandi Kodi Anda. Ini dapat ditemukan di dalam Sistem/Setelan/Jaringan/Layanan." + }, + "discovery_confirm": { + "description": "Ingin menambahkan Kodi (`{name}`) to Home Assistant?", + "title": "Kodi yang ditemukan" + }, + "user": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL" + }, + "description": "Informasi koneksi Kodi. Pastikan untuk mengaktifkan \"Izinkan kontrol Kodi melalui HTTP\" di Sistem/Pengaturan/Jaringan/Layanan." + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "Port WebSocket (kadang-kadang disebut port TCP di Kodi). Untuk terhubung melalui WebSocket, Anda perlu mengaktifkan \"Izinkan program... untuk mengontrol Kodi\" dalam Sistem/Pengaturan/Jaringan/Layanan. Jika WebSocket tidak diaktifkan, hapus port dan biarkan kosong." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} diminta untuk dimatikan", + "turn_on": "{entity_name} diminta untuk dinyalakan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ko.json b/homeassistant/components/kodi/translations/ko.json index 233cd068a1e..3c5a1a38e96 100644 --- a/homeassistant/components/kodi/translations/ko.json +++ b/homeassistant/components/kodi/translations/ko.json @@ -4,6 +4,7 @@ "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", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_uuid": "Kodi \uc778\uc2a4\ud134\uc2a4\uc5d0 \uace0\uc720\ud55c ID\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774\ub294 \uc624\ub798\ub41c Kodi \ubc84\uc804(17.x \uc774\ud558) \ub54c\ubb38\uc77c \uac00\ub2a5\uc131\uc774 \ub192\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud558\uac70\ub098 \ucd5c\uc2e0 Kodi \ubc84\uc804\uc73c\ub85c \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\ubcf4\uc138\uc694.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -18,11 +19,11 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "Kodi \uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624. \uc774\ub7ec\ud55c \ub0b4\uc6a9\uc740 \uc2dc\uc2a4\ud15c/\uc124\uc815/\ub124\ud2b8\uc6cc\ud06c/\uc11c\ube44\uc2a4\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "description": "Kodi \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc124\uc815/\uc11c\ube44\uc2a4/\ucee8\ud2b8\ub864/\uc6f9 \uc11c\ubc84\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "discovery_confirm": { - "description": "Kodi (` {name} `)\ub97c Home Assistant\uc5d0 \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Kodi \ubc1c\uacac" + "description": "Home Assistant\uc5d0 Kodi (`{name}`)\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c Kodi" }, "user": { "data": { @@ -30,14 +31,20 @@ "port": "\ud3ec\ud2b8", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9" }, - "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4. \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c Kodi \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624." + "description": "Kodi \uc5f0\uacb0 \uc815\ubcf4\uc785\ub2c8\ub2e4. \uc124\uc815/\uc11c\ube44\uc2a4/\ucee8\ud2b8\ub864/\uc6f9 \uc11c\ubc84\uc5d0\uc11c \"HTTP\ub97c \ud1b5\ud55c \uc6d0\uaca9 \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "ws_port": { "data": { "ws_port": "\ud3ec\ud2b8" }, - "description": "WebSocket \ud3ec\ud2b8 (Kodi\uc5d0\uc11c TCP \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568). WebSocket\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc2dc\uc2a4\ud15c / \uc124\uc815 / \ub124\ud2b8\uc6cc\ud06c / \uc11c\ube44\uc2a4\uc5d0\uc11c \"\ud504\ub85c\uadf8\ub7a8\uc774 Kodi\ub97c \uc81c\uc5b4\ud558\ub3c4\ub85d \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c\ud569\ub2c8\ub2e4. WebSocket\uc774 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc \ub461\ub2c8\ub2e4." + "description": "\uc6f9 \uc18c\ucf13 \ud3ec\ud2b8(Kodi\uc5d0\uc11c\ub294 \ud3ec\ud2b8\ub77c\uace0\ub3c4 \ud568)\uc785\ub2c8\ub2e4. \uc6f9 \uc18c\ucf13\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815/\uc11c\ube44\uc2a4/\ucee8\ud2b8\ub864/\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ucee8\ud2b8\ub864\uc5d0\uc11c \"\uc774 \uc2dc\uc2a4\ud15c\uc758/\ub2e4\ub978 \uc2dc\uc2a4\ud15c\uc758 \ud504\ub85c\uadf8\ub7a8\uc5d0 \uc758\ud55c \uc6d0\uaca9 \uc81c\uc5b4 \ud5c8\uc6a9\"\uc744 \ud65c\uc131\ud654\ud574\uc57c \ud569\ub2c8\ub2e4. \uc6f9 \uc18c\ucf13\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\ub294 \uacbd\uc6b0 \ud3ec\ud2b8\ub97c \uc81c\uac70\ud558\uace0 \ube44\uc6cc\ub450\uc138\uc694." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name}\uc774(\uac00) \uaebc\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c", + "turn_on": "{entity_name}\uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json index 57476791b8f..4143d933d19 100644 --- a/homeassistant/components/kodi/translations/nl.json +++ b/homeassistant/components/kodi/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie", + "no_uuid": "Kodi-instantie heeft geen unieke ID. Dit komt waarschijnlijk door een oude Kodi-versie (17.x of lager). U kunt de integratie handmatig configureren of upgraden naar een recentere Kodi-versie.", "unknown": "Onverwachte fout" }, "error": { @@ -39,5 +40,11 @@ "description": "De WebSocket-poort (ook wel TCP-poort genoemd in Kodi). Om verbinding te maken via WebSocket, moet u \"Programma's toestaan ... om Kodi te besturen\" inschakelen in Systeem / Instellingen / Netwerk / Services. Als WebSocket niet is ingeschakeld, verwijdert u de poort en laat u deze leeg." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} werd gevraagd om uit te schakelen", + "turn_on": "{entity_name} is gevraagd om in te schakelen" + } } } \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json index 50742417f28..b6f7443f061 100644 --- a/homeassistant/components/kodi/translations/ru.json +++ b/homeassistant/components/kodi/translations/ru.json @@ -17,7 +17,7 @@ "credentials": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c Kodi. \u0418\u0445 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438, \u043f\u0435\u0440\u0435\u0439\u0434\u044f \u0432 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\"." }, diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 348eaeda3ac..db1e20204cd 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -261,9 +261,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # async_connect will handle retries until it establishes a connection await client.async_connect() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) # config entry specific data to enable unload @@ -278,8 +278,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -360,8 +360,14 @@ class KonnectedView(HomeAssistantView): try: zone_num = str(payload.get(CONF_ZONE) or PIN_TO_ZONE[payload[CONF_PIN]]) payload[CONF_ZONE] = zone_num - zone_data = device[CONF_BINARY_SENSORS].get(zone_num) or next( - (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None + zone_data = ( + device[CONF_BINARY_SENSORS].get(zone_num) + or next( + (s for s in device[CONF_SWITCHES] if s[CONF_ZONE] == zone_num), None + ) + or next( + (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None + ) ) except KeyError: zone_data = None diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index 923e5d63899..879d0d4cf8f 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -18,7 +18,7 @@ HANDLERS = decorator.Registry() @HANDLERS.register("state") async def async_handle_state_update(hass, context, msg): - """Handle a binary sensor state update.""" + """Handle a binary sensor or switch state update.""" _LOGGER.debug("[state handler] context: %s msg: %s", context, msg) entity_id = context.get(ATTR_ENTITY_ID) state = bool(int(msg.get(ATTR_STATE))) diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index 18f2ed64a1d..cf2f33de332 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -376,7 +376,7 @@ class AlarmPanel: self.async_desired_settings_payload() != self.async_current_settings_payload() ): - _LOGGER.info("pushing settings to device %s", self.device_id) + _LOGGER.info("Pushing settings to device %s", self.device_id) await self.client.put_settings(**self.async_desired_settings_payload()) diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index dece8d06c87..18975bdb467 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -1,4 +1,5 @@ """Support for DHT and DS18B20 sensors attached to a Konnected device.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_DEVICES, CONF_NAME, @@ -12,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_DS18B20_NEW @@ -70,7 +70,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, SIGNAL_DS18B20_NEW, async_add_ds18b20) -class KonnectedSensor(Entity): +class KonnectedSensor(SensorEntity): """Represents a Konnected DHT Sensor.""" def __init__(self, device_id, data, sensor_type, addr=None, initial_state=None): diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index b599fe55242..9c9f8193dcd 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -9,6 +9,8 @@ from homeassistant.const import ( CONF_SWITCHES, CONF_ZONE, ) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity from .const import ( @@ -130,6 +132,16 @@ class KonnectedSwitch(ToggleEntity): state, ) + @callback + def async_set_state(self, state): + """Update the switch state.""" + self._set_state(state) + async def async_added_to_hass(self): - """Store entity_id.""" + """Store entity_id and register state change callback.""" self._data["entity_id"] = self.entity_id + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"konnected.{self.entity_id}.update", self.async_set_state + ) + ) diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 1cc44a02646..507e5d258f2 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { @@ -11,6 +19,11 @@ }, "options": { "step": { + "options_binary": { + "data": { + "name": "N\u00e9v (nem k\u00f6telez\u0151)" + } + }, "options_digital": { "data": { "name": "N\u00e9v (nem k\u00f6telez\u0151)", diff --git a/homeassistant/components/konnected/translations/id.json b/homeassistant/components/konnected/translations/id.json new file mode 100644 index 00000000000..633e6bba2df --- /dev/null +++ b/homeassistant/components/konnected/translations/id.json @@ -0,0 +1,108 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "not_konn_panel": "Bukan perangkat Konnected.io yang dikenali", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "confirm": { + "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nAnda dapat mengonfigurasi IO dan perilaku panel di pengaturan Panel Alarm Konnected.", + "title": "Perangkat Konnected Siap" + }, + "import_confirm": { + "description": "Panel Alarm Konnected dengan ID {id} telah ditemukan di configuration.yaml. Aliran ini akan memungkinkan Anda mengimpornya menjadi entri konfigurasi.", + "title": "Impor Perangkat Konnected" + }, + "user": { + "data": { + "host": "Alamat IP", + "port": "Port" + }, + "description": "Masukkan informasi host untuk Panel Konnected Anda." + } + } + }, + "options": { + "abort": { + "not_konn_panel": "Bukan perangkat Konnected.io yang dikenali" + }, + "error": { + "bad_host": "URL host API yang Menimpa Tidak Valid" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Balikkan status buka/tutup", + "name": "Nama (opsional)", + "type": "Jenis Sensor Biner" + }, + "description": "Opsi {zone}", + "title": "Konfigurasikan Sensor Biner" + }, + "options_digital": { + "data": { + "name": "Nama (opsional)", + "poll_interval": "Interval Polling (dalam menit) (opsional)", + "type": "Jenis Sensor" + }, + "description": "Opsi {zone}", + "title": "Konfigurasi Sensor Digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "OUT" + }, + "description": "Ditemukan {model} di {host}. Pilih konfigurasi dasar setiap I/O di bawah ini - tergantung pada I/O yang mungkin memungkinkan sensor biner (kontak terbuka/tutup), sensor digital (dht dan ds18b20), atau sakelar output. Anda dapat mengonfigurasi opsi terperinci dalam langkah berikutnya.", + "title": "Konfigurasikan I/O" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Pilih konfigurasi I/O lainnya di bawah ini. Anda dapat mengonfigurasi detail opsi pada langkah berikutnya.", + "title": "Konfigurasikan I/O yang Diperluas" + }, + "options_misc": { + "data": { + "api_host": "Ganti URL host API (opsional)", + "blink": "Kedipkan panel LED saat mengirim perubahan status", + "discovery": "Tanggapi permintaan penemuan di jaringan Anda", + "override_api_host": "Timpa URL panel host API Home Assistant bawaan" + }, + "description": "Pilih perilaku yang diinginkan untuk panel Anda", + "title": "Konfigurasikan Lainnya" + }, + "options_switch": { + "data": { + "activation": "Keluaran saat nyala", + "momentary": "Durasi pulsa (milidetik) (opsional)", + "more_states": "Konfigurasikan status tambahan untuk zona ini", + "name": "Nama (opsional)", + "pause": "Jeda di antara pulsa (milidetik) (opsional)", + "repeat": "Waktu pengulangan (-1 = tak terbatas) (opsional)" + }, + "description": "Opsi {zone}: status {state}", + "title": "Konfigurasikan Output yang Dapat Dialihkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json index fe5b9a0347a..eb7fa3de4e0 100644 --- a/homeassistant/components/konnected/translations/ko.json +++ b/homeassistant/components/konnected/translations/ko.json @@ -15,7 +15,7 @@ "title": "Konnected \uae30\uae30 \uc900\ube44" }, "import_confirm": { - "description": "Konnected \uc54c\ub78c \ud328\ub110 ID {id} \uac00 configuration.yaml \uc5d0\uc11c \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \uacfc\uc815\uc744 \ud1b5\ud574 \uad6c\uc131 \ud56d\ubaa9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "Konnected \uc54c\ub78c \ud328\ub110 ID {id}\uac00 configuration.yaml\uc5d0\uc11c \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \uacfc\uc815\uc744 \ud1b5\ud574 \uad6c\uc131 \ud56d\ubaa9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "Konnected \uae30\uae30 \uac00\uc838\uc624\uae30" }, "user": { @@ -85,7 +85,7 @@ "data": { "api_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758 (\uc120\ud0dd \uc0ac\ud56d)", "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4", - "discovery": "\ub124\ud2b8\uc6cc\ud06c\uc758 \uac80\uc0c9 \uc694\uccad\uc5d0 \uc751\ub2f5", + "discovery": "\ub124\ud2b8\uc6cc\ud06c\uc758 \uac80\uc0c9 \uc694\uccad\uc5d0 \uc751\ub2f5\ud558\uae30", "override_api_host": "\uae30\ubcf8 Home Assistant API \ud638\uc2a4\ud2b8 \ud328\ub110 URL \uc7ac\uc815\uc758" }, "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index 9a7f20ac1e1..0387dc8c7b0 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom voor het apparaat wordt al uitgevoerd.", + "already_in_progress": "De configuratiestroom is al aan de gang", "not_konn_panel": "Geen herkend Konnected.io apparaat", - "unknown": "Onbekende fout opgetreden" + "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Kan geen verbinding maken met een Konnected Panel op {host} : {port}" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "confirm": { @@ -20,8 +20,8 @@ }, "user": { "data": { - "host": "IP-adres van Konnected apparaat", - "port": "Konnected apparaat poort" + "host": "IP-adres", + "port": "Poort" }, "description": "Voer de host-informatie in voor uw Konnected-paneel." } @@ -48,7 +48,7 @@ }, "options_digital": { "data": { - "name": "Naam (optioneel)", + "name": "Naam (optional)", "poll_interval": "Poll interval (minuten) (optioneel)", "type": "Type sensor" }, @@ -87,6 +87,7 @@ "data": { "api_host": "API host-URL overschrijven (optioneel)", "blink": "Led knipperen bij het verzenden van statuswijziging", + "discovery": "Reageer op detectieverzoeken op uw netwerk", "override_api_host": "Overschrijf standaard Home Assistant API hostpaneel-URL" }, "description": "Selecteer het gewenste gedrag voor uw paneel", diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 9459d44805c..951e2a5353f 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -4,7 +4,7 @@ import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN PLATFORMS = ["light"] @@ -16,9 +16,14 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Kuler Sky from a config entry.""" - for component in PLATFORMS: + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + 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, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -26,15 +31,18 @@ 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( + # Stop discovery + unregister_discovery = hass.data[DOMAIN].pop(DATA_DISCOVERY_SUBSCRIPTION, None) + if unregister_discovery: + unregister_discovery() + + hass.data.pop(DOMAIN, None) + + return all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py index 04f7719b8e6..2a11a3c2e17 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -15,9 +15,7 @@ async def _async_has_devices(hass) -> bool: """Return if there are devices that can be discovered.""" # Check if there are any devices that can be discovered in the network. try: - devices = await hass.async_add_executor_job( - pykulersky.discover_bluetooth_devices - ) + devices = await pykulersky.discover() except pykulersky.PykulerskyException as exc: _LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc) return False diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py index ae1e7a435dc..8b314d7bde9 100644 --- a/homeassistant/components/kulersky/const.py +++ b/homeassistant/components/kulersky/const.py @@ -1,2 +1,5 @@ """Constants for the Kuler Sky integration.""" DOMAIN = "kulersky" + +DATA_ADDRESSES = "addresses" +DATA_DISCOVERY_SUBSCRIPTION = "discovery_subscription" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 71dd4a158ca..980d4612ce9 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -1,8 +1,9 @@ """Kuler Sky light platform.""" -import asyncio +from __future__ import annotations + from datetime import timedelta import logging -from typing import Callable, List +from typing import Callable import pykulersky @@ -22,7 +23,7 @@ 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 DOMAIN +from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,68 +31,39 @@ SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE DISCOVERY_INTERVAL = timedelta(seconds=60) -PARALLEL_UPDATES = 0 - - -def check_light(light: pykulersky.Light): - """Attempt to connect to this light and read the color.""" - light.connect() - light.get_color() - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Kuler sky light devices.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - if "devices" not in hass.data[DOMAIN]: - hass.data[DOMAIN]["devices"] = set() - if "discovery" not in hass.data[DOMAIN]: - hass.data[DOMAIN]["discovery"] = asyncio.Lock() async def discover(*args): """Attempt to discover new lights.""" - # Since discovery needs to connect to all discovered bluetooth devices, and - # only rules out devices after a timeout, it can potentially take a long - # time. If there's already a discovery running, just skip this poll. - if hass.data[DOMAIN]["discovery"].locked(): - return + lights = await pykulersky.discover() - async with hass.data[DOMAIN]["discovery"]: - bluetooth_devices = await hass.async_add_executor_job( - pykulersky.discover_bluetooth_devices - ) + # Filter out already discovered lights + new_lights = [ + light + for light in lights + if light.address not in hass.data[DOMAIN][DATA_ADDRESSES] + ] - # Filter out already connected lights - new_devices = [ - device - for device in bluetooth_devices - if device["address"] not in hass.data[DOMAIN]["devices"] - ] + new_entities = [] + for light in new_lights: + hass.data[DOMAIN][DATA_ADDRESSES].add(light.address) + new_entities.append(KulerskyLight(light)) - for device in new_devices: - light = pykulersky.Light(device["address"], device["name"]) - try: - # If the connection fails, either this is not a Kuler Sky - # light, or it's bluetooth connection is currently locked - # by another device. If the vendor's app is connected to - # the light when home assistant tries to connect, this - # connection will fail. - await hass.async_add_executor_job(check_light, light) - except pykulersky.PykulerskyException: - continue - # The light has successfully connected - hass.data[DOMAIN]["devices"].add(device["address"]) - async_add_entities([KulerskyLight(light)], update_before_add=True) + async_add_entities(new_entities, update_before_add=True) # Start initial discovery hass.async_create_task(discover()) # Perform recurring discovery of new devices - async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + hass.data[DOMAIN][DATA_DISCOVERY_SUBSCRIPTION] = async_track_time_interval( + hass, discover, DISCOVERY_INTERVAL + ) class KulerskyLight(LightEntity): @@ -103,21 +75,24 @@ class KulerskyLight(LightEntity): self._hs_color = None self._brightness = None self._white_value = None - self._available = True + self._available = None async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" self.async_on_remove( - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_will_remove_from_hass + ) ) - async def async_will_remove_from_hass(self) -> None: + async def async_will_remove_from_hass(self, *args) -> None: """Run when entity will be removed from hass.""" - await self.hass.async_add_executor_job(self.disconnect) - - def disconnect(self, *args) -> None: - """Disconnect the underlying device.""" - self._light.disconnect() + try: + await self._light.disconnect() + except pykulersky.PykulerskyException: + _LOGGER.debug( + "Exception disconnected from %s", self._light.address, exc_info=True + ) @property def name(self): @@ -168,7 +143,7 @@ class KulerskyLight(LightEntity): """Return True if entity is available.""" return self._available - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" default_hs = (0, 0) if self._hs_color is None else self._hs_color hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) @@ -187,28 +162,28 @@ class KulerskyLight(LightEntity): rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) - self._light.set_color(*rgb, white_value) + await self._light.set_color(*rgb, white_value) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._light.set_color(0, 0, 0, 0) + await self._light.set_color(0, 0, 0, 0) - def update(self): + async def async_update(self): """Fetch new state data for this light.""" try: - if not self._light.connected: - self._light.connect() + if not self._available: + await self._light.connect() # pylint: disable=invalid-name - r, g, b, w = self._light.get_color() + r, g, b, w = await self._light.get_color() except pykulersky.PykulerskyException as exc: if self._available: _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc) self._available = False return - if not self._available: - _LOGGER.info("Reconnected to %s", self.entity_id) - self._available = True + if self._available is False: + _LOGGER.info("Reconnected to %s", self._light.address) + self._available = True hsv = color_util.color_RGB_to_hsv(r, g, b) self._hs_color = hsv[:2] self._brightness = int(round((hsv[2] / 100) * 255)) diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json index 4f445e4fc18..b690d94e8d4 100644 --- a/homeassistant/components/kulersky/manifest.json +++ b/homeassistant/components/kulersky/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kulersky", "requirements": [ - "pykulersky==0.4.0" + "pykulersky==0.5.2" ], "codeowners": [ "@emlove" diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json index 96ed09a974f..86bc8e36730 100644 --- a/homeassistant/components/kulersky/translations/de.json +++ b/homeassistant/components/kulersky/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/kulersky/translations/hu.json b/homeassistant/components/kulersky/translations/hu.json index 3d5be90042e..6c61530acbe 100644 --- a/homeassistant/components/kulersky/translations/hu.json +++ b/homeassistant/components/kulersky/translations/hu.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "step": { "confirm": { - "description": "El akarod kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/kulersky/translations/id.json b/homeassistant/components/kulersky/translations/id.json new file mode 100644 index 00000000000..223836a8b40 --- /dev/null +++ b/homeassistant/components/kulersky/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ko.json b/homeassistant/components/kulersky/translations/ko.json index 7011a61f757..e5ae04d6e5c 100644 --- a/homeassistant/components/kulersky/translations/ko.json +++ b/homeassistant/components/kulersky/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index bd0430f9786..eb96b206653 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -2,7 +2,7 @@ from pykwb import kwb import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -11,7 +11,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity DEFAULT_RAW = False DEFAULT_NAME = "KWB" @@ -74,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class KWBSensor(Entity): +class KWBSensor(SensorEntity): """Representation of a KWB Easyfire sensor.""" def __init__(self, easyfire, sensor, client_name): diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 2c7f5d294a9..32090797f11 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -6,7 +6,11 @@ import pylacrosse from serial import SerialException import voluptuous as vol -from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE, CONF_ID, @@ -19,7 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -78,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.warning("Unable to open serial port: %s", exc) return False - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lacrosse.close) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: lacrosse.close()) if CONF_JEELINK_LED in config: lacrosse.led_mode_state(config.get(CONF_JEELINK_LED)) @@ -108,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class LaCrosseSensor(Entity): +class LaCrosseSensor(SensorEntity): """Implementation of a Lacrosse sensor.""" _temperature = None @@ -138,7 +142,7 @@ class LaCrosseSensor(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attributes = { "low_battery": self._low_battery, diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 9fe3a182844..e732b5d7000 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -2,6 +2,6 @@ "domain": "lastfm", "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": ["pylast==4.1.0"], + "requirements": ["pylast==4.2.0"], "codeowners": [] } diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 56124e2c0fe..128450826d6 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -7,10 +7,9 @@ import pylast as lastfm from pylast import WSError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class LastfmSensor(Entity): +class LastfmSensor(SensorEntity): """A class for the Last.fm account.""" def __init__(self, user, lastfm_api): @@ -107,7 +106,7 @@ class LastfmSensor(Entity): self._state = f"{now_playing.artist} - {now_playing.title}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index ef816eef0ba..831e44dca8f 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -1,16 +1,16 @@ """A sensor platform that give you information about the next space launch.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional from pylaunches import PyLaunches, PyLaunchesException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from .const import ( ATTR_AGENCY, @@ -39,7 +39,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([LaunchLibrarySensor(launches, name)], True) -class LaunchLibrarySensor(Entity): +class LaunchLibrarySensor(SensorEntity): """Representation of a launch_library Sensor.""" def __init__(self, launches: PyLaunches, name: str) -> None: @@ -64,7 +64,7 @@ class LaunchLibrarySensor(Entity): return self._name @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the sensor.""" if self.next_launch: return self.next_launch.name @@ -76,7 +76,7 @@ class LaunchLibrarySensor(Entity): return "mdi:rocket" @property - def device_state_attributes(self) -> Optional[dict]: + def extra_state_attributes(self) -> dict | None: """Return attributes for the sensor.""" if self.next_launch: return { diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index cc1e47d71fc..9384fbed29d 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,137 +1,162 @@ """Support for LCN devices.""" +import asyncio import logging import pypck +from homeassistant import config_entries from homeassistant.const import ( - CONF_BINARY_SENSORS, - CONF_COVERS, - CONF_HOST, - CONF_LIGHTS, + CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SENSORS, - CONF_SWITCHES, + CONF_RESOURCE, CONF_USERNAME, ) -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import Entity -from .const import ( - CONF_CLIMATES, - CONF_CONNECTIONS, - CONF_DIM_MODE, - CONF_SCENES, - CONF_SK_NUM_TRIES, - DATA_LCN, - DOMAIN, -) -from .schemas import CONFIG_SCHEMA # noqa: 401 -from .services import ( - DynText, - Led, - LockKeys, - LockRegulator, - OutputAbs, - OutputRel, - OutputToggle, - Pck, - Relays, - SendKeys, - VarAbs, - VarRel, - VarReset, -) +from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, CONNECTION, DOMAIN +from .helpers import generate_unique_id, import_lcn_config +from .schemas import CONFIG_SCHEMA # noqa: F401 +from .services import SERVICES + +PLATFORMS = ["binary_sensor", "climate", "cover", "light", "scene", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up the LCN component.""" - hass.data[DATA_LCN] = {} + if DOMAIN not in config: + return True - conf_connections = config[DOMAIN][CONF_CONNECTIONS] - connections = [] - for conf_connection in conf_connections: - connection_name = conf_connection.get(CONF_NAME) + # initialize a config_flow for all LCN configurations read from + # configuration.yaml + config_entries_data = import_lcn_config(config[DOMAIN]) - settings = { - "SK_NUM_TRIES": conf_connection[CONF_SK_NUM_TRIES], - "DIM_MODE": pypck.lcn_defs.OutputPortDimMode[ - conf_connection[CONF_DIM_MODE] - ], - } - - connection = pypck.connection.PchkConnectionManager( - conf_connection[CONF_HOST], - conf_connection[CONF_PORT], - conf_connection[CONF_USERNAME], - conf_connection[CONF_PASSWORD], - settings=settings, - connection_id=connection_name, - ) - - try: - # establish connection to PCHK server - await hass.async_create_task(connection.async_connect(timeout=15)) - connections.append(connection) - _LOGGER.info('LCN connected to "%s"', connection_name) - except TimeoutError: - _LOGGER.error('Connection to PCHK server "%s" failed', connection_name) - return False - - hass.data[DATA_LCN][CONF_CONNECTIONS] = connections - - # load platforms - for component, conf_key in ( - ("binary_sensor", CONF_BINARY_SENSORS), - ("climate", CONF_CLIMATES), - ("cover", CONF_COVERS), - ("light", CONF_LIGHTS), - ("scene", CONF_SCENES), - ("sensor", CONF_SENSORS), - ("switch", CONF_SWITCHES), - ): - if conf_key in config[DOMAIN]: - hass.async_create_task( - async_load_platform( - hass, component, DOMAIN, config[DOMAIN][conf_key], config - ) + for config_entry_data in config_entries_data: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config_entry_data, ) + ) + return True + + +async def async_setup_entry(hass, config_entry): + """Set up a connection to PCHK host from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + if config_entry.entry_id in hass.data[DOMAIN]: + return False + + settings = { + "SK_NUM_TRIES": config_entry.data[CONF_SK_NUM_TRIES], + "DIM_MODE": pypck.lcn_defs.OutputPortDimMode[config_entry.data[CONF_DIM_MODE]], + } + + # connect to PCHK + lcn_connection = pypck.connection.PchkConnectionManager( + config_entry.data[CONF_IP_ADDRESS], + config_entry.data[CONF_PORT], + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + settings=settings, + connection_id=config_entry.entry_id, + ) + try: + # establish connection to PCHK server + await lcn_connection.async_connect(timeout=15) + except pypck.connection.PchkAuthenticationError: + _LOGGER.warning('Authentication on PCHK "%s" failed', config_entry.title) + return False + except pypck.connection.PchkLicenseError: + _LOGGER.warning( + 'Maximum number of connections on PCHK "%s" was ' + "reached. An additional license key is required", + config_entry.title, + ) + return False + except TimeoutError: + _LOGGER.warning('Connection to PCHK "%s" failed', config_entry.title) + return False + + _LOGGER.debug('LCN connected to "%s"', config_entry.title) + hass.data[DOMAIN][config_entry.entry_id] = { + CONNECTION: lcn_connection, + } + + # remove orphans from entity registry which are in ConfigEntry but were removed + # from configuration.yaml + if config_entry.source == config_entries.SOURCE_IMPORT: + entity_registry = await er.async_get_registry(hass) + 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) + ) # register service calls - for service_name, service in ( - ("output_abs", OutputAbs), - ("output_rel", OutputRel), - ("output_toggle", OutputToggle), - ("relays", Relays), - ("var_abs", VarAbs), - ("var_reset", VarReset), - ("var_rel", VarRel), - ("lock_regulator", LockRegulator), - ("led", Led), - ("send_keys", SendKeys), - ("lock_keys", LockKeys), - ("dyn_text", DynText), - ("pck", Pck), - ): - hass.services.async_register( - DOMAIN, service_name, service(hass).async_call_service, service.schema - ) + for service_name, service in SERVICES: + if not hass.services.has_service(DOMAIN, service_name): + hass.services.async_register( + DOMAIN, service_name, service(hass).async_call_service, service.schema + ) return True -class LcnEntity(Entity): - """Parent class for all devices associated with the LCN component.""" +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 + ] + ) + ) - def __init__(self, config, device_connection): + if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: + host = hass.data[DOMAIN].pop(config_entry.entry_id) + await host[CONNECTION].async_close() + + # unregister service calls + if unload_ok and not hass.data[DOMAIN]: # check if this is the last entry to unload + for service_name, _ in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + + return unload_ok + + +class LcnEntity(Entity): + """Parent class for all entities associated with the LCN component.""" + + def __init__(self, config, entry_id, device_connection): """Initialize the LCN device.""" self.config = config + self.entry_id = entry_id self.device_connection = device_connection + self._unregister_for_inputs = None self._name = config[CONF_NAME] + @property + def unique_id(self): + """Return a unique ID.""" + unique_device_id = generate_unique_id( + ( + self.device_connection.seg_id, + self.device_connection.addr_id, + self.device_connection.is_group, + ) + ) + return f"{self.entry_id}-{unique_device_id}-{self.config[CONF_RESOURCE]}" + @property def should_poll(self): """Lcn device entity pushes its state to HA.""" @@ -140,7 +165,14 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" if not self.device_connection.is_group: - self.device_connection.register_for_inputs(self.input_received) + self._unregister_for_inputs = self.device_connection.register_for_inputs( + self.input_received + ) + + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + if self._unregister_for_inputs is not None: + self._unregister_for_inputs() @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 56a5ea6e646..3bea502cc76 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -1,49 +1,56 @@ """Support for LCN binary sensors.""" import pypck -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_ADDRESS, CONF_SOURCE +from homeassistant.components.binary_sensor import ( + DOMAIN as DOMAIN_BINARY_SENSOR, + BinarySensorEntity, +) +from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE from . import LcnEntity -from .const import BINSENSOR_PORTS, CONF_CONNECTIONS, DATA_LCN, SETPOINTS -from .helpers import get_connection +from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, SETPOINTS +from .helpers import get_device_connection -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Set up the LCN binary sensor platform.""" - if discovery_info is None: - return +def create_lcn_binary_sensor_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + if entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] in SETPOINTS: + return LcnRegulatorLockSensor( + entity_config, config_entry.entry_id, device_connection + ) + if entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] in BINSENSOR_PORTS: + return LcnBinarySensor(entity_config, config_entry.entry_id, device_connection) + # in KEY + return LcnLockKeysSensor(entity_config, config_entry.entry_id, device_connection) - if config[CONF_SOURCE] in SETPOINTS: - device = LcnRegulatorLockSensor(config, address_connection) - elif config[CONF_SOURCE] in BINSENSOR_PORTS: - device = LcnBinarySensor(config, address_connection) - else: # in KEYS - device = LcnLockKeysSensor(config, address_connection) - devices.append(device) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN switch entities from a config entry.""" + entities = [] - async_add_entities(devices) + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_BINARY_SENSOR: + entities.append( + create_lcn_binary_sensor_entity(hass, entity_config, config_entry) + ) + + async_add_entities(entities) class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for regulator locks.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN binary sensor.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.setpoint_variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] + self.setpoint_variable = pypck.lcn_defs.Var[ + config[CONF_DOMAIN_DATA][CONF_SOURCE] + ] self._value = None @@ -55,6 +62,14 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): self.setpoint_variable ) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler( + self.setpoint_variable + ) + @property def is_on(self): """Return true if the binary sensor is on.""" @@ -75,11 +90,13 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): class LcnBinarySensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for binary sensor ports.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN binary sensor.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.bin_sensor_port = pypck.lcn_defs.BinSensorPort[config[CONF_SOURCE]] + self.bin_sensor_port = pypck.lcn_defs.BinSensorPort[ + config[CONF_DOMAIN_DATA][CONF_SOURCE] + ] self._value = None @@ -91,6 +108,14 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): self.bin_sensor_port ) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler( + self.bin_sensor_port + ) + @property def is_on(self): """Return true if the binary sensor is on.""" @@ -108,11 +133,11 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN sensor for key locks.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.source = pypck.lcn_defs.Key[config[CONF_SOURCE]] + self.source = pypck.lcn_defs.Key[config[CONF_DOMAIN_DATA][CONF_SOURCE]] self._value = None async def async_added_to_hass(self): @@ -121,6 +146,12 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.source) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.source) + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index e3269a51cd6..056abcda2b0 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -1,68 +1,76 @@ """Support for LCN climate control.""" - import pypck -from homeassistant.components.climate import ClimateEntity, const +from homeassistant.components.climate import ( + DOMAIN as DOMAIN_CLIMATE, + ClimateEntity, + const, +) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, + CONF_DOMAIN, + CONF_ENTITIES, CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT, ) from . import LcnEntity from .const import ( - CONF_CONNECTIONS, + CONF_DOMAIN_DATA, CONF_LOCKABLE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_SETPOINT, - DATA_LCN, ) -from .helpers import get_connection +from .helpers import get_device_connection PARALLEL_UPDATES = 0 -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Set up the LCN climate platform.""" - if discovery_info is None: - return +def create_lcn_climate_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + return LcnClimate(entity_config, config_entry.entry_id, device_connection) - devices.append(LcnClimate(config, address_connection)) - async_add_entities(devices) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN switch entities from a config entry.""" + entities = [] + + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_CLIMATE: + entities.append( + create_lcn_climate_entity(hass, entity_config, config_entry) + ) + + async_add_entities(entities) class LcnClimate(LcnEntity, ClimateEntity): """Representation of a LCN climate device.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize of a LCN climate device.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] - self.setpoint = pypck.lcn_defs.Var[config[CONF_SETPOINT]] - self.unit = pypck.lcn_defs.VarUnit.parse(config[CONF_UNIT_OF_MEASUREMENT]) + self.variable = pypck.lcn_defs.Var[config[CONF_DOMAIN_DATA][CONF_SOURCE]] + self.setpoint = pypck.lcn_defs.Var[config[CONF_DOMAIN_DATA][CONF_SETPOINT]] + self.unit = pypck.lcn_defs.VarUnit.parse( + config[CONF_DOMAIN_DATA][CONF_UNIT_OF_MEASUREMENT] + ) self.regulator_id = pypck.lcn_defs.Var.to_set_point_id(self.setpoint) - self.is_lockable = config[CONF_LOCKABLE] - self._max_temp = config[CONF_MAX_TEMP] - self._min_temp = config[CONF_MIN_TEMP] + self.is_lockable = config[CONF_DOMAIN_DATA][CONF_LOCKABLE] + self._max_temp = config[CONF_DOMAIN_DATA][CONF_MAX_TEMP] + self._min_temp = config[CONF_DOMAIN_DATA][CONF_MIN_TEMP] self._current_temperature = None self._target_temperature = None - self._is_on = None + self._is_on = True async def async_added_to_hass(self): """Run when entity about to be added to hass.""" @@ -71,6 +79,13 @@ class LcnClimate(LcnEntity, ClimateEntity): await self.device_connection.activate_status_request_handler(self.variable) await self.device_connection.activate_status_request_handler(self.setpoint) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.variable) + await self.device_connection.cancel_status_request_handler(self.setpoint) + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py new file mode 100644 index 00000000000..fe353cdb4c5 --- /dev/null +++ b/homeassistant/components/lcn/config_flow.py @@ -0,0 +1,97 @@ +"""Config flow to configure the LCN integration.""" +import logging + +import pypck + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def get_config_entry(hass, data): + """Check config entries for already configured entries based on the ip address/port.""" + return next( + ( + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.data[CONF_IP_ADDRESS] == data[CONF_IP_ADDRESS] + and entry.data[CONF_PORT] == data[CONF_PORT] + ), + None, + ) + + +async def validate_connection(host_name, data): + """Validate if a connection to LCN can be established.""" + host = data[CONF_IP_ADDRESS] + port = data[CONF_PORT] + username = data[CONF_USERNAME] + password = data[CONF_PASSWORD] + sk_num_tries = data[CONF_SK_NUM_TRIES] + dim_mode = data[CONF_DIM_MODE] + + settings = { + "SK_NUM_TRIES": sk_num_tries, + "DIM_MODE": pypck.lcn_defs.OutputPortDimMode[dim_mode], + } + + _LOGGER.debug("Validating connection parameters to PCHK host '%s'", host_name) + + connection = pypck.connection.PchkConnectionManager( + host, port, username, password, settings=settings + ) + + await connection.async_connect(timeout=5) + + _LOGGER.debug("LCN connection validated") + await connection.async_close() + return data + + +@config_entries.HANDLERS.register(DOMAIN) +class LcnFlowHandler(config_entries.ConfigFlow): + """Handle a LCN config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_import(self, data): + """Import existing configuration from LCN.""" + host_name = data[CONF_HOST] + # validate the imported connection parameters + try: + await validate_connection(host_name, data) + except pypck.connection.PchkAuthenticationError: + _LOGGER.warning('Authentication on PCHK "%s" failed', host_name) + return self.async_abort(reason="authentication_error") + except pypck.connection.PchkLicenseError: + _LOGGER.warning( + 'Maximum number of connections on PCHK "%s" was ' + "reached. An additional license key is required", + host_name, + ) + return self.async_abort(reason="license_error") + except TimeoutError: + _LOGGER.warning('Connection to PCHK "%s" failed', host_name) + return self.async_abort(reason="connection_timeout") + + # check if we already have a host with the same address configured + entry = get_config_entry(self.hass, data) + if entry: + entry.source = config_entries.SOURCE_IMPORT + self.hass.config_entries.async_update_entry(entry, data=data) + return self.async_abort(reason="existing_configuration_updated") + + return self.async_create_entry( + title=f"{host_name}", + data=data, + ) diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 3dcac6fb55f..4e3e765ace0 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -14,6 +14,13 @@ DOMAIN = "lcn" DATA_LCN = "lcn" DEFAULT_NAME = "pchk" +CONNECTION = "connection" +CONF_HARDWARE_SERIAL = "hardware_serial" +CONF_SOFTWARE_SERIAL = "software_serial" +CONF_HARDWARE_TYPE = "hardware_type" +CONF_RESOURCE = "resource" +CONF_DOMAIN_DATA = "domain_data" + CONF_CONNECTIONS = "connections" CONF_SK_NUM_TRIES = "sk_num_tries" CONF_OUTPUT = "output" diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 3d7c2a06a3b..bf777ad93f2 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,53 +1,54 @@ """Support for LCN covers.""" + import pypck -from homeassistant.components.cover import CoverEntity -from homeassistant.const import CONF_ADDRESS +from homeassistant.components.cover import DOMAIN as DOMAIN_COVER, CoverEntity +from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES from . import LcnEntity -from .const import CONF_CONNECTIONS, CONF_MOTOR, CONF_REVERSE_TIME, DATA_LCN -from .helpers import get_connection +from .const import CONF_DOMAIN_DATA, CONF_MOTOR, CONF_REVERSE_TIME +from .helpers import get_device_connection PARALLEL_UPDATES = 0 -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Setups the LCN cover platform.""" - if discovery_info is None: - return +def create_lcn_cover_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + if entity_config[CONF_DOMAIN_DATA][CONF_MOTOR] in "OUTPUTS": + return LcnOutputsCover(entity_config, config_entry.entry_id, device_connection) + # in RELAYS + return LcnRelayCover(entity_config, config_entry.entry_id, device_connection) - if config[CONF_MOTOR] == "OUTPUTS": - devices.append(LcnOutputsCover(config, address_connection)) - else: # RELAYS - devices.append(LcnRelayCover(config, address_connection)) - async_add_entities(devices) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN cover entities from a config entry.""" + entities = [] + + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_COVER: + entities.append(create_lcn_cover_entity(hass, entity_config, config_entry)) + + async_add_entities(entities) class LcnOutputsCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to output ports.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN cover.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) self.output_ids = [ pypck.lcn_defs.OutputPort["OUTPUTUP"].value, pypck.lcn_defs.OutputPort["OUTPUTDOWN"].value, ] - if CONF_REVERSE_TIME in config: + if CONF_REVERSE_TIME in config[CONF_DOMAIN_DATA]: self.reverse_time = pypck.lcn_defs.MotorReverseTime[ - config[CONF_REVERSE_TIME] + config[CONF_DOMAIN_DATA][CONF_REVERSE_TIME] ] else: self.reverse_time = None @@ -59,12 +60,24 @@ class LcnOutputsCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - pypck.lcn_defs.OutputPort["OUTPUTUP"] - ) - await self.device_connection.activate_status_request_handler( - pypck.lcn_defs.OutputPort["OUTPUTDOWN"] - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + pypck.lcn_defs.OutputPort["OUTPUTUP"] + ) + await self.device_connection.activate_status_request_handler( + pypck.lcn_defs.OutputPort["OUTPUTDOWN"] + ) + + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler( + pypck.lcn_defs.OutputPort["OUTPUTUP"] + ) + await self.device_connection.cancel_status_request_handler( + pypck.lcn_defs.OutputPort["OUTPUTDOWN"] + ) @property def is_closed(self): @@ -146,11 +159,11 @@ class LcnOutputsCover(LcnEntity, CoverEntity): class LcnRelayCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to relays.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN cover.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.motor = pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] + self.motor = pypck.lcn_defs.MotorPort[config[CONF_DOMAIN_DATA][CONF_MOTOR]] self.motor_port_onoff = self.motor.value * 2 self.motor_port_updown = self.motor_port_onoff + 1 @@ -164,6 +177,12 @@ class LcnRelayCover(LcnEntity, CoverEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.motor) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.motor) + @property def is_closed(self): """Return if the cover is closed.""" diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 18342aa1d98..3f93ec95a69 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -1,11 +1,42 @@ """Helpers for LCN component.""" import re +import pypck import voluptuous as vol -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONF_ADDRESS, + CONF_BINARY_SENSORS, + CONF_COVERS, + CONF_DEVICES, + CONF_DOMAIN, + CONF_ENTITIES, + CONF_HOST, + CONF_IP_ADDRESS, + CONF_LIGHTS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_USERNAME, +) -from .const import DEFAULT_NAME +from .const import ( + CONF_CLIMATES, + CONF_CONNECTIONS, + CONF_DIM_MODE, + CONF_DOMAIN_DATA, + CONF_HARDWARE_SERIAL, + CONF_HARDWARE_TYPE, + CONF_RESOURCE, + CONF_SCENES, + CONF_SK_NUM_TRIES, + CONF_SOFTWARE_SERIAL, + CONNECTION, + DEFAULT_NAME, + DOMAIN, +) # Regex for address validation PATTERN_ADDRESS = re.compile( @@ -13,17 +44,145 @@ PATTERN_ADDRESS = re.compile( ) -def get_connection(connections, connection_id=None): - """Return the connection object from list.""" - if connection_id is None: - connection = connections[0] - else: - for connection in connections: - if connection.connection_id == connection_id: - break - else: - raise ValueError("Unknown connection_id.") - return connection +DOMAIN_LOOKUP = { + CONF_BINARY_SENSORS: "binary_sensor", + CONF_CLIMATES: "climate", + CONF_COVERS: "cover", + CONF_LIGHTS: "light", + CONF_SCENES: "scene", + CONF_SENSORS: "sensor", + CONF_SWITCHES: "switch", +} + + +def get_device_connection(hass, address, config_entry): + """Return a lcn device_connection.""" + host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] + addr = pypck.lcn_addr.LcnAddr(*address) + return host_connection.get_address_conn(addr) + + +def get_resource(domain_name, domain_data): + """Return the resource for the specified domain_data.""" + if domain_name in ["switch", "light"]: + return domain_data["output"] + if domain_name in ["binary_sensor", "sensor"]: + return domain_data["source"] + if domain_name == "cover": + return domain_data["motor"] + if domain_name == "climate": + return f'{domain_data["source"]}.{domain_data["setpoint"]}' + if domain_name == "scene": + return f'{domain_data["register"]}.{domain_data["scene"]}' + raise ValueError("Unknown domain") + + +def generate_unique_id(address): + """Generate a unique_id from the given parameters.""" + is_group = "g" if address[2] else "m" + return f"{is_group}{address[0]:03d}{address[1]:03d}" + + +def import_lcn_config(lcn_config): + """Convert lcn settings from configuration.yaml to config_entries data. + + Create a list of config_entry data structures like: + + "data": { + "host": "pchk", + "ip_address": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn, + "sk_num_tries: 0, + "dim_mode: "STEPS200", + "devices": [ + { + "address": (0, 7, False) + "name": "", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 + }, ... + ], + "entities": [ + { + "address": (0, 7, False) + "name": "Light_Output1", + "resource": "output1", + "domain": "light", + "domain_data": { + "output": "OUTPUT1", + "dimmable": True, + "transition": 5000.0 + } + }, ... + ] + } + """ + data = {} + for connection in lcn_config[CONF_CONNECTIONS]: + host = { + CONF_HOST: connection[CONF_NAME], + CONF_IP_ADDRESS: connection[CONF_HOST], + CONF_PORT: connection[CONF_PORT], + CONF_USERNAME: connection[CONF_USERNAME], + CONF_PASSWORD: connection[CONF_PASSWORD], + CONF_SK_NUM_TRIES: connection[CONF_SK_NUM_TRIES], + CONF_DIM_MODE: connection[CONF_DIM_MODE], + CONF_DEVICES: [], + CONF_ENTITIES: [], + } + data[connection[CONF_NAME]] = host + + for confkey, domain_config in lcn_config.items(): + if confkey == CONF_CONNECTIONS: + continue + domain = DOMAIN_LOOKUP[confkey] + # loop over entities in configuration.yaml + for domain_data in domain_config: + # remove name and address from domain_data + entity_name = domain_data.pop(CONF_NAME) + address, host_name = domain_data.pop(CONF_ADDRESS) + + if host_name is None: + host_name = DEFAULT_NAME + + # check if we have a new device config + for device_config in data[host_name][CONF_DEVICES]: + if address == device_config[CONF_ADDRESS]: + break + else: # create new device_config + device_config = { + CONF_ADDRESS: address, + CONF_NAME: "", + CONF_HARDWARE_SERIAL: -1, + CONF_SOFTWARE_SERIAL: -1, + CONF_HARDWARE_TYPE: -1, + } + + data[host_name][CONF_DEVICES].append(device_config) + + # insert entity config + resource = get_resource(domain, domain_data).lower() + for entity_config in data[host_name][CONF_ENTITIES]: + if ( + address == entity_config[CONF_ADDRESS] + and resource == entity_config[CONF_RESOURCE] + and domain == entity_config[CONF_DOMAIN] + ): + break + else: # create new entity_config + entity_config = { + CONF_ADDRESS: address, + CONF_NAME: entity_name, + CONF_RESOURCE: resource, + CONF_DOMAIN: domain, + CONF_DOMAIN_DATA: domain_data.copy(), + } + data[host_name][CONF_ENTITIES].append(entity_config) + + return list(data.values()) def has_unique_host_names(hosts): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 8a76056ff46..8697d8e0319 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,68 +1,69 @@ """Support for LCN lights.""" + import pypck from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, + DOMAIN as DOMAIN_LIGHT, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.const import CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES from . import LcnEntity from .const import ( - CONF_CONNECTIONS, CONF_DIMMABLE, + CONF_DOMAIN_DATA, CONF_OUTPUT, CONF_TRANSITION, - DATA_LCN, OUTPUT_PORTS, ) -from .helpers import get_connection +from .helpers import get_device_connection PARALLEL_UPDATES = 0 -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Set up the LCN light platform.""" - if discovery_info is None: - return +def create_lcn_light_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS: + return LcnOutputLight(entity_config, config_entry.entry_id, device_connection) + # in RELAY_PORTS + return LcnRelayLight(entity_config, config_entry.entry_id, device_connection) - if config[CONF_OUTPUT] in OUTPUT_PORTS: - device = LcnOutputLight(config, address_connection) - else: # in RELAY_PORTS - device = LcnRelayLight(config, address_connection) - devices.append(device) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN light entities from a config entry.""" + entities = [] - async_add_entities(devices) + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_LIGHT: + entities.append(create_lcn_light_entity(hass, entity_config, config_entry)) + + async_add_entities(entities) class LcnOutputLight(LcnEntity, LightEntity): """Representation of a LCN light for output ports.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN light.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - self._transition = pypck.lcn_defs.time_to_ramp_value(config[CONF_TRANSITION]) - self.dimmable = config[CONF_DIMMABLE] + self._transition = pypck.lcn_defs.time_to_ramp_value( + config[CONF_DOMAIN_DATA][CONF_TRANSITION] + ) + self.dimmable = config[CONF_DOMAIN_DATA][CONF_DIMMABLE] self._brightness = 255 - self._is_on = None + self._is_on = False self._is_dimming_to_zero = False async def async_added_to_hass(self): @@ -71,6 +72,12 @@ class LcnOutputLight(LcnEntity, LightEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.output) + @property def supported_features(self): """Flag supported features.""" @@ -145,13 +152,13 @@ class LcnOutputLight(LcnEntity, LightEntity): class LcnRelayLight(LcnEntity, LightEntity): """Representation of a LCN light for relay ports.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN light.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] - self._is_on = None + self._is_on = False async def async_added_to_hass(self): """Run when entity about to be added to hass.""" @@ -159,6 +166,12 @@ class LcnRelayLight(LcnEntity, LightEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.output) + @property def is_on(self): """Return True if entity is on.""" diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index c5077bdf409..5c8be5829e0 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -1,6 +1,7 @@ { "domain": "lcn", "name": "LCN", + "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", "requirements": [ "pypck==0.7.9" diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index 1c359607fb2..8f770df7668 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -1,72 +1,69 @@ """Support for LCN scenes.""" -from typing import Any import pypck -from homeassistant.components.scene import Scene -from homeassistant.const import CONF_ADDRESS, CONF_SCENE +from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene +from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE from . import LcnEntity from .const import ( - CONF_CONNECTIONS, + CONF_DOMAIN_DATA, CONF_OUTPUTS, CONF_REGISTER, CONF_TRANSITION, - DATA_LCN, OUTPUT_PORTS, ) -from .helpers import get_connection +from .helpers import get_device_connection PARALLEL_UPDATES = 0 -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Set up the LCN scene platform.""" - if discovery_info is None: - return +def create_lcn_scene_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + return LcnScene(entity_config, config_entry.entry_id, device_connection) - devices.append(LcnScene(config, address_connection)) - async_add_entities(devices) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN switch entities from a config entry.""" + entities = [] + + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_SCENE: + entities.append(create_lcn_scene_entity(hass, entity_config, config_entry)) + + async_add_entities(entities) class LcnScene(LcnEntity, Scene): """Representation of a LCN scene.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN scene.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.register_id = config[CONF_REGISTER] - self.scene_id = config[CONF_SCENE] + self.register_id = config[CONF_DOMAIN_DATA][CONF_REGISTER] + self.scene_id = config[CONF_DOMAIN_DATA][CONF_SCENE] self.output_ports = [] self.relay_ports = [] - for port in config[CONF_OUTPUTS]: + for port in config[CONF_DOMAIN_DATA][CONF_OUTPUTS]: if port in OUTPUT_PORTS: self.output_ports.append(pypck.lcn_defs.OutputPort[port]) else: # in RELEAY_PORTS self.relay_ports.append(pypck.lcn_defs.RelayPort[port]) - if config[CONF_TRANSITION] is None: + if config[CONF_DOMAIN_DATA][CONF_TRANSITION] is None: self.transition = None else: - self.transition = pypck.lcn_defs.time_to_ramp_value(config[CONF_TRANSITION]) + self.transition = pypck.lcn_defs.time_to_ramp_value( + config[CONF_DOMAIN_DATA][CONF_TRANSITION] + ) - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - - async def async_activate(self, **kwargs: Any) -> None: + async def async_activate(self, **kwargs): """Activate scene.""" await self.device_connection.activate_scene( self.register_id, diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 11932dccea8..64870e22e4c 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,55 +1,67 @@ """Support for LCN sensors.""" + import pypck -from homeassistant.const import CONF_ADDRESS, CONF_SOURCE, CONF_UNIT_OF_MEASUREMENT +from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR, SensorEntity +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DOMAIN, + CONF_ENTITIES, + CONF_SOURCE, + CONF_UNIT_OF_MEASUREMENT, +) from . import LcnEntity from .const import ( - CONF_CONNECTIONS, - DATA_LCN, + CONF_DOMAIN_DATA, LED_PORTS, S0_INPUTS, SETPOINTS, THRESHOLDS, VARIABLES, ) -from .helpers import get_connection +from .helpers import get_device_connection -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Set up the LCN sensor platform.""" - if discovery_info is None: - return +def create_lcn_sensor_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - device_connection = connection.get_address_conn(addr) - - if config[CONF_SOURCE] in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS: - device = LcnVariableSensor(config, device_connection) - else: # in LED_PORTS + LOGICOP_PORTS - device = LcnLedLogicSensor(config, device_connection) - - devices.append(device) - - async_add_entities(devices) + if ( + entity_config[CONF_DOMAIN_DATA][CONF_SOURCE] + in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS + ): + return LcnVariableSensor( + entity_config, config_entry.entry_id, device_connection + ) + # in LED_PORTS + LOGICOP_PORTS + return LcnLedLogicSensor(entity_config, config_entry.entry_id, device_connection) -class LcnVariableSensor(LcnEntity): +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN switch entities from a config entry.""" + entities = [] + + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_SENSOR: + entities.append(create_lcn_sensor_entity(hass, entity_config, config_entry)) + + async_add_entities(entities) + + +class LcnVariableSensor(LcnEntity, SensorEntity): """Representation of a LCN sensor for variables.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] - self.unit = pypck.lcn_defs.VarUnit.parse(config[CONF_UNIT_OF_MEASUREMENT]) + self.variable = pypck.lcn_defs.Var[config[CONF_DOMAIN_DATA][CONF_SOURCE]] + self.unit = pypck.lcn_defs.VarUnit.parse( + config[CONF_DOMAIN_DATA][CONF_UNIT_OF_MEASUREMENT] + ) self._value = None @@ -59,6 +71,12 @@ class LcnVariableSensor(LcnEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.variable) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.variable) + @property def state(self): """Return the state of the entity.""" @@ -81,17 +99,19 @@ class LcnVariableSensor(LcnEntity): self.async_write_ha_state() -class LcnLedLogicSensor(LcnEntity): +class LcnLedLogicSensor(LcnEntity, SensorEntity): """Representation of a LCN sensor for leds and logicops.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - if config[CONF_SOURCE] in LED_PORTS: - self.source = pypck.lcn_defs.LedPort[config[CONF_SOURCE]] + if config[CONF_DOMAIN_DATA][CONF_SOURCE] in LED_PORTS: + self.source = pypck.lcn_defs.LedPort[config[CONF_DOMAIN_DATA][CONF_SOURCE]] else: - self.source = pypck.lcn_defs.LogicOpPort[config[CONF_SOURCE]] + self.source = pypck.lcn_defs.LogicOpPort[ + config[CONF_DOMAIN_DATA][CONF_SOURCE] + ] self._value = None @@ -101,6 +121,12 @@ class LcnLedLogicSensor(LcnEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.source) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.source) + @property def state(self): """Return the state of the entity.""" diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index d7d8acf4f29..c6c33270264 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, + CONF_HOST, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, TIME_SECONDS, @@ -13,7 +14,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from .const import ( - CONF_CONNECTIONS, CONF_KEYS, CONF_LED, CONF_OUTPUT, @@ -28,7 +28,7 @@ from .const import ( CONF_TRANSITION, CONF_VALUE, CONF_VARIABLE, - DATA_LCN, + DOMAIN, LED_PORTS, LED_STATUS, OUTPUT_PORTS, @@ -41,7 +41,7 @@ from .const import ( VARIABLES, ) from .helpers import ( - get_connection, + get_device_connection, is_address, is_key_lock_states_string, is_relays_states_string, @@ -56,18 +56,20 @@ class LcnServiceCall: def __init__(self, hass): """Initialize service call.""" self.hass = hass - self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS] def get_device_connection(self, service): - """Get device connection object.""" - addr, connection_id = service.data[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*addr) - if connection_id is None: - connection = self.connections[0] - else: - connection = get_connection(self.connections, connection_id) + """Get address connection object.""" + address, host_name = service.data[CONF_ADDRESS] - return connection.get_address_conn(addr) + for config_entry in self.hass.config_entries.async_entries(DOMAIN): + if config_entry.data[CONF_HOST] == host_name: + device_connection = get_device_connection( + self.hass, address, config_entry + ) + if device_connection is None: + raise ValueError("Wrong address.") + return device_connection + raise ValueError("Invalid host name.") async def async_call_service(self, service): """Execute service call.""" @@ -392,3 +394,20 @@ class Pck(LcnServiceCall): pck = service.data[CONF_PCK] device_connection = self.get_device_connection(service) await device_connection.pck(pck) + + +SERVICES = ( + ("output_abs", OutputAbs), + ("output_rel", OutputRel), + ("output_toggle", OutputToggle), + ("relays", Relays), + ("var_abs", VarAbs), + ("var_reset", VarReset), + ("var_rel", VarRel), + ("lock_regulator", LockRegulator), + ("led", Led), + ("send_keys", SendKeys), + ("lock_keys", LockKeys), + ("dyn_text", DynText), + ("pck", Pck), +) diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 5fe624b04bf..1429bf67f7e 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,49 +1,49 @@ """Support for LCN switches.""" + import pypck -from homeassistant.components.switch import SwitchEntity -from homeassistant.const import CONF_ADDRESS +from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity +from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_ENTITIES from . import LcnEntity -from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS -from .helpers import get_connection +from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS +from .helpers import get_device_connection PARALLEL_UPDATES = 0 -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): - """Set up the LCN switch platform.""" - if discovery_info is None: - return +def create_lcn_switch_entity(hass, entity_config, config_entry): + """Set up an entity for this domain.""" + device_connection = get_device_connection( + hass, tuple(entity_config[CONF_ADDRESS]), config_entry + ) - devices = [] - for config in discovery_info: - address, connection_id = config[CONF_ADDRESS] - addr = pypck.lcn_addr.LcnAddr(*address) - connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + if entity_config[CONF_DOMAIN_DATA][CONF_OUTPUT] in OUTPUT_PORTS: + return LcnOutputSwitch(entity_config, config_entry.entry_id, device_connection) + # in RELAY_PORTS + return LcnRelaySwitch(entity_config, config_entry.entry_id, device_connection) - if config[CONF_OUTPUT] in OUTPUT_PORTS: - device = LcnOutputSwitch(config, address_connection) - else: # in RELAY_PORTS - device = LcnRelaySwitch(config, address_connection) - devices.append(device) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up LCN switch entities from a config entry.""" - async_add_entities(devices) + entities = [] + + for entity_config in config_entry.data[CONF_ENTITIES]: + if entity_config[CONF_DOMAIN] == DOMAIN_SWITCH: + entities.append(create_lcn_switch_entity(hass, entity_config, config_entry)) + + async_add_entities(entities) class LcnOutputSwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for output ports.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN switch.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.OutputPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] self._is_on = None @@ -53,6 +53,12 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.output) + @property def is_on(self): """Return True if entity is on.""" @@ -87,11 +93,11 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): class LcnRelaySwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for relay ports.""" - def __init__(self, config, device_connection): + def __init__(self, config, entry_id, device_connection): """Initialize the LCN switch.""" - super().__init__(config, device_connection) + super().__init__(config, entry_id, device_connection) - self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] + self.output = pypck.lcn_defs.RelayPort[config[CONF_DOMAIN_DATA][CONF_OUTPUT]] self._is_on = None @@ -101,6 +107,12 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): if not self.device_connection.is_group: await self.device_connection.activate_status_request_handler(self.output) + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + await super().async_will_remove_from_hass() + if not self.device_connection.is_group: + await self.device_connection.cancel_status_request_handler(self.output) + @property def is_on(self): """Return True if entity is on.""" diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index c2d196196f9..78bf7e050ef 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -159,7 +159,8 @@ class LGDevice(MediaPlayerEntity): """Return the available sound modes.""" modes = [] for equaliser in self._equalisers: - modes.append(temescal.equalisers[equaliser]) + if equaliser < len(temescal.equalisers): + modes.append(temescal.equalisers[equaliser]) return sorted(modes) @property @@ -174,7 +175,8 @@ class LGDevice(MediaPlayerEntity): """List of available input sources.""" sources = [] for function in self._functions: - sources.append(temescal.functions[function]) + if function < len(temescal.functions): + sources.append(temescal.functions[function]) return sorted(sources) @property diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/life360/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 086e3ebf7d2..603efee6d9d 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -1,15 +1,17 @@ { "config": { "abort": { - "unknown": "V\u00e1ratlan hiba" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "create_entry": { - "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3] ( {docs_url} )." + "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3]({docs_url})." }, "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", - "unknown": "V\u00e1ratlan hiba" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index 2bb7a1cca68..21a93366c44 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -1,7 +1,27 @@ { "config": { + "abort": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "create_entry": { + "default": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url})." + }, "error": { - "invalid_username": "Nama pengguna tidak valid" + "already_configured": "Akun sudah dikonfigurasi", + "invalid_auth": "Autentikasi tidak valid", + "invalid_username": "Nama pengguna tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url}).\nAnda mungkin ingin melakukannya sebelum menambahkan akun.", + "title": "Info Akun Life360" + } } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 5b5934fbb42..b7bc7198987 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -10,14 +10,14 @@ "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", + "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Life360" diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index f06a7720bb2..9f1c5747aa8 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -4,7 +4,6 @@ from datetime import timedelta from functools import partial import logging import math -import sys import aiolifx as aiolifx_module import aiolifx_effects as aiolifx_effects_module @@ -166,12 +165,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up LIFX from a config entry.""" - if sys.platform == "win32": - _LOGGER.warning( - "The lifx platform is known to not work on Windows. " - "Consider using the lifx_legacy platform instead" - ) - # Priority 1: manual config interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN) if not interfaces: diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index 0b6cdb39fd4..f706dcefa96 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Nem tal\u00e1lhat\u00f3k LIFX eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", - "single_instance_allowed": "Csak egyetlen LIFX konfigur\u00e1ci\u00f3 lehets\u00e9ges." + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/id.json b/homeassistant/components/lifx/translations/id.json new file mode 100644 index 00000000000..03b2b387c6f --- /dev/null +++ b/homeassistant/components/lifx/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan LIFX?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/ko.json b/homeassistant/components/lifx/translations/ko.json index 34bec9c3aee..4d388cbeda2 100644 --- a/homeassistant/components/lifx/translations/ko.json +++ b/homeassistant/components/lifx/translations/ko.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { - "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "LIFX\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json index 60efcdffa46..0e0a6190f0d 100644 --- a/homeassistant/components/lifx/translations/nl.json +++ b/homeassistant/components/lifx/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Geen LIFX-apparaten gevonden op het netwerk.", - "single_instance_allowed": "Slechts een enkele configuratie van LIFX is mogelijk." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py index f0ed9105b99..795f3e17793 100644 --- a/homeassistant/components/lifx_legacy/light.py +++ b/homeassistant/components/lifx_legacy/light.py @@ -46,13 +46,22 @@ SUPPORT_LIFX = ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_BROADCAST): cv.string} +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_SERVER), + cv.deprecated(CONF_BROADCAST), + PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_SERVER): cv.string, vol.Optional(CONF_BROADCAST): cv.string} + ), ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LIFX platform.""" + _LOGGER.warning( + "The LIFX Legacy platform is deprecated and will be removed in " + "Home Assistant Core 2021.6.0; Use the LIFX integration instead" + ) + server_addr = config.get(CONF_SERVER) broadcast_addr = config.get(CONF_BROADCAST) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 55476c754f2..4fae5caab00 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -6,7 +6,7 @@ import dataclasses from datetime import timedelta import logging import os -from typing import Dict, List, Optional, Tuple, cast +from typing import cast, final import voluptuous as vol @@ -16,7 +16,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -25,7 +25,6 @@ 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 HomeAssistantType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -38,19 +37,49 @@ DATA_PROFILES = "light_profiles" ENTITY_ID_FORMAT = DOMAIN + ".{}" # Bitfield of features supported by the light entity -SUPPORT_BRIGHTNESS = 1 -SUPPORT_COLOR_TEMP = 2 +SUPPORT_BRIGHTNESS = 1 # Deprecated, replaced by color modes +SUPPORT_COLOR_TEMP = 2 # Deprecated, replaced by color modes SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 -SUPPORT_COLOR = 16 +SUPPORT_COLOR = 16 # Deprecated, replaced by color modes SUPPORT_TRANSITION = 32 -SUPPORT_WHITE_VALUE = 128 +SUPPORT_WHITE_VALUE = 128 # Deprecated, replaced by color modes + +# Color mode of the light +ATTR_COLOR_MODE = "color_mode" +# List of color modes supported by the light +ATTR_SUPPORTED_COLOR_MODES = "supported_color_modes" +# Possible color modes +COLOR_MODE_UNKNOWN = "unknown" # Ambiguous color mode +COLOR_MODE_ONOFF = "onoff" # Must be the only supported mode +COLOR_MODE_BRIGHTNESS = "brightness" # Must be the only supported mode +COLOR_MODE_COLOR_TEMP = "color_temp" +COLOR_MODE_HS = "hs" +COLOR_MODE_XY = "xy" +COLOR_MODE_RGB = "rgb" +COLOR_MODE_RGBW = "rgbw" +COLOR_MODE_RGBWW = "rgbww" + +VALID_COLOR_MODES = { + COLOR_MODE_ONOFF, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_XY, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, +} +COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} +COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY} # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" # Lists holding color values ATTR_RGB_COLOR = "rgb_color" +ATTR_RGBW_COLOR = "rgbw_color" +ATTR_RGBWW_COLOR = "rgbww_color" ATTR_XY_COLOR = "xy_color" ATTR_HS_COLOR = "hs_color" ATTR_COLOR_TEMP = "color_temp" @@ -104,7 +133,13 @@ LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.byte,) * 4), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.byte,) * 5), vol.Coerce(tuple) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) @@ -166,14 +201,6 @@ def preprocess_turn_on_alternatives(hass, params): if brightness_pct is not None: params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100) - xy_color = params.pop(ATTR_XY_COLOR, None) - if xy_color is not None: - params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) - - rgb_color = params.pop(ATTR_RGB_COLOR, None) - if rgb_color is not None: - params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) - def filter_turn_off_params(params): """Filter out params not used in turn off.""" @@ -207,7 +234,7 @@ async def async_setup(hass, config): If brightness is set to 0, this service will turn the light off. """ - params = call.data["params"] + params = dict(call.data["params"]) # Only process params once we processed brightness step if params and ( @@ -228,6 +255,52 @@ async def async_setup(hass, config): if ATTR_PROFILE not in params: profiles.apply_default(light.entity_id, params) + supported_color_modes = light.supported_color_modes + # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W + # for legacy lights + if ATTR_RGBW_COLOR in params: + legacy_supported_color_modes = ( + light._light_internal_supported_color_modes # pylint: disable=protected-access + ) + if ( + COLOR_MODE_RGBW in legacy_supported_color_modes + and not supported_color_modes + ): + rgbw_color = params.pop(ATTR_RGBW_COLOR) + params[ATTR_RGB_COLOR] = rgbw_color[0:3] + params[ATTR_WHITE_VALUE] = rgbw_color[3] + + # If a color is specified, convert to the color space supported by the light + # Backwards compatibility: Fall back to hs color if light.supported_color_modes + # is not implemented + if not supported_color_modes: + if (rgb_color := params.pop(ATTR_RGB_COLOR, None)) is not None: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif (xy_color := params.pop(ATTR_XY_COLOR, None)) is not None: + params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + elif ATTR_HS_COLOR in params and COLOR_MODE_HS not in supported_color_modes: + 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_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: + 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) + elif ATTR_XY_COLOR in params and COLOR_MODE_XY not in supported_color_modes: + xy_color = params.pop(ATTR_XY_COLOR) + if COLOR_MODE_HS in supported_color_modes: + 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) + + # 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)) @@ -290,11 +363,11 @@ class Profile: """Representation of a profile.""" name: str - color_x: Optional[float] = dataclasses.field(repr=False) - color_y: Optional[float] = dataclasses.field(repr=False) - brightness: Optional[int] - transition: Optional[int] = None - hs_color: Optional[Tuple[float, float]] = dataclasses.field(init=False) + color_x: float | None = dataclasses.field(repr=False) + color_y: float | None = dataclasses.field(repr=False) + brightness: int | None + transition: int | None = None + hs_color: tuple[float, float] | None = dataclasses.field(init=False) SCHEMA = vol.Schema( # pylint: disable=invalid-name vol.Any( @@ -329,7 +402,7 @@ class Profile: ) @classmethod - def from_csv_row(cls, csv_row: List[str]) -> Profile: + def from_csv_row(cls, csv_row: list[str]) -> Profile: """Create profile from a CSV row tuple.""" return cls(*cls.SCHEMA(csv_row)) @@ -337,12 +410,12 @@ class Profile: class Profiles: """Representation of available color profiles.""" - def __init__(self, hass: HomeAssistantType): + def __init__(self, hass: HomeAssistant): """Initialize profiles.""" self.hass = hass - self.data: Dict[str, Profile] = {} + self.data: dict[str, Profile] = {} - def _load_profile_data(self) -> Dict[str, Profile]: + def _load_profile_data(self) -> dict[str, Profile]: """Load built-in profiles and custom profiles.""" profile_paths = [ os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), @@ -379,7 +452,7 @@ 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: + def apply_default(self, entity_id: str, params: dict) -> None: """Return the default turn-on profile for the given light.""" for _entity_id in (entity_id, "group.all_lights"): name = f"{_entity_id}.default" @@ -388,7 +461,7 @@ class Profiles: return @callback - def apply_profile(self, name: str, params: Dict) -> None: + def apply_profile(self, name: str, params: dict) -> None: """Apply a profile.""" profile = self.data.get(name) @@ -404,49 +477,121 @@ class Profiles: class LightEntity(ToggleEntity): - """Representation of a light.""" + """Base class for light entities.""" @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return None @property - def hs_color(self): + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return None + + @property + def _light_internal_color_mode(self) -> str: + """Return the color mode of the light with backwards compatibility.""" + color_mode = self.color_mode + + if color_mode is None: + # Backwards compatibility for color_mode added in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + supported = self._light_internal_supported_color_modes + + if ( + COLOR_MODE_RGBW in supported + and self.white_value is not None + and self.hs_color is not None + ): + return COLOR_MODE_RGBW + if COLOR_MODE_HS in supported and self.hs_color is not None: + return COLOR_MODE_HS + if COLOR_MODE_COLOR_TEMP in supported and self.color_temp is not None: + return COLOR_MODE_COLOR_TEMP + if COLOR_MODE_BRIGHTNESS in supported and self.brightness is not None: + return COLOR_MODE_BRIGHTNESS + if COLOR_MODE_ONOFF in supported: + return COLOR_MODE_ONOFF + return COLOR_MODE_UNKNOWN + + return color_mode + + @property + def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" return None @property - def color_temp(self): + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + return None + + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + return None + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + return None + + @property + def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + rgbw_color = self.rgbw_color + if ( + rgbw_color is None + and self.hs_color is not None + and self.white_value is not None + ): + # Backwards compatibility for rgbw_color added in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + r, g, b = color_util.color_hs_to_RGB( # pylint: disable=invalid-name + *self.hs_color + ) + w = self.white_value # pylint: disable=invalid-name + rgbw_color = (r, g, b, w) + + return rgbw_color + + @property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value [int, int, int, int, int].""" + return None + + @property + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" return None @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed # https://developers.meethue.com/documentation/core-concepts return 153 @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed # https://developers.meethue.com/documentation/core-concepts return 500 @property - def white_value(self): + def white_value(self) -> int | None: """Return the white value of this light between 0..255.""" return None @property - def effect_list(self): + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return None @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" return None @@ -463,8 +608,32 @@ class LightEntity(ToggleEntity): if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list + data[ATTR_SUPPORTED_COLOR_MODES] = sorted( + self._light_internal_supported_color_modes + ) + return data + def _light_internal_convert_color(self, color_mode: str) -> dict: + data: dict[str, tuple] = {} + if color_mode == COLOR_MODE_HS and self.hs_color: + hs_color = self.hs_color + data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + elif color_mode == COLOR_MODE_XY and self.xy_color: + xy_color = self.xy_color + data[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + data[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color) + data[ATTR_XY_COLOR] = (round(xy_color[0], 6), round(xy_color[1], 6)) + elif color_mode == COLOR_MODE_RGB and self.rgb_color: + rgb_color = self.rgb_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_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + return data + + @final @property def state_attributes(self): """Return state attributes.""" @@ -473,21 +642,49 @@ class LightEntity(ToggleEntity): data = {} supported_features = self.supported_features + color_mode = self._light_internal_color_mode - if supported_features & SUPPORT_BRIGHTNESS: + if color_mode not in self._light_internal_supported_color_modes: + # Increase severity to warning in 2021.6, reject in 2021.10 + _LOGGER.debug( + "%s: set to unsupported color_mode: %s, supported_color_modes: %s", + self.entity_id, + color_mode, + self._light_internal_supported_color_modes, + ) + + data[ATTR_COLOR_MODE] = color_mode + + if color_mode in COLOR_MODES_BRIGHTNESS: + data[ATTR_BRIGHTNESS] = self.brightness + elif supported_features & SUPPORT_BRIGHTNESS: + # Backwards compatibility for ambiguous / incomplete states + # Add warning in 2021.6, remove in 2021.10 data[ATTR_BRIGHTNESS] = self.brightness - if supported_features & SUPPORT_COLOR_TEMP: + if color_mode == COLOR_MODE_COLOR_TEMP: data[ATTR_COLOR_TEMP] = self.color_temp - if supported_features & SUPPORT_COLOR and self.hs_color: - hs_color = self.hs_color - data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) - data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) - data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + if color_mode in COLOR_MODES_COLOR: + data.update(self._light_internal_convert_color(color_mode)) - if supported_features & SUPPORT_WHITE_VALUE: + 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 + data[ATTR_COLOR_TEMP] = self.color_temp + + if supported_features & SUPPORT_WHITE_VALUE and not self.supported_color_modes: + # Backwards compatibility + # Add warning in 2021.6, remove in 2021.10 data[ATTR_WHITE_VALUE] = self.white_value + if self.hs_color is not None: + data.update(self._light_internal_convert_color(COLOR_MODE_HS)) if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT] = self.effect @@ -495,7 +692,37 @@ class LightEntity(ToggleEntity): return {key: val for key, val in data.items() if val is not None} @property - def supported_features(self): + def _light_internal_supported_color_modes(self) -> set: + """Calculate supported color modes with backwards compatibility.""" + supported_color_modes = self.supported_color_modes + + if supported_color_modes is None: + # Backwards compatibility for supported_color_modes added in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + supported_features = self.supported_features + supported_color_modes = set() + + if supported_features & SUPPORT_COLOR_TEMP: + supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + if supported_features & SUPPORT_COLOR: + supported_color_modes.add(COLOR_MODE_HS) + if supported_features & SUPPORT_WHITE_VALUE: + supported_color_modes.add(COLOR_MODE_RGBW) + if supported_features & SUPPORT_BRIGHTNESS and not supported_color_modes: + supported_color_modes = {COLOR_MODE_BRIGHTNESS} + + if not supported_color_modes: + supported_color_modes = {COLOR_MODE_ONOFF} + + return supported_color_modes + + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return None + + @property + def supported_features(self) -> int: """Flag supported features.""" return 0 diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index d499bc0c2a2..4c37647f168 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -1,5 +1,5 @@ """Provides device actions for lights.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -78,7 +78,7 @@ async def async_call_action_from_config( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions.""" actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 1d9323907f2..7396ddeea31 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for lights.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ def async_condition_from_config( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 066d1f4c020..e1b14124831 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -1,5 +1,5 @@ """Provides device trigger for lights.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ async def async_attach_trigger( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py index 1636054663d..234883ffd5a 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/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/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index a7939beb91e..68fac8aa5c9 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -1,8 +1,10 @@ """Reproduce an Light state.""" +from __future__ import annotations + import asyncio import logging from types import MappingProxyType -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -11,12 +13,12 @@ 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 ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_MODE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, @@ -25,9 +27,18 @@ from . import ( ATTR_KELVIN, ATTR_PROFILE, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_XY, DOMAIN, ) @@ -48,6 +59,8 @@ COLOR_GROUP = [ ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_XY_COLOR, # The following color attributes are deprecated ATTR_PROFILE, @@ -55,6 +68,15 @@ COLOR_GROUP = [ ATTR_KELVIN, ] +COLOR_MODE_TO_ATTRIBUTE = { + COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP, + COLOR_MODE_HS: ATTR_HS_COLOR, + COLOR_MODE_RGB: ATTR_RGB_COLOR, + COLOR_MODE_RGBW: ATTR_RGBW_COLOR, + COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR, + COLOR_MODE_XY: ATTR_XY_COLOR, +} + DEPRECATED_GROUP = [ ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, @@ -71,11 +93,11 @@ DEPRECATION_WARNING = ( async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -102,7 +124,7 @@ async def _async_reproduce_state( ): return - service_data: Dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id} + service_data: dict[str, Any] = {ATTR_ENTITY_ID: state.entity_id} if reproduce_options is not None and ATTR_TRANSITION in reproduce_options: service_data[ATTR_TRANSITION] = reproduce_options[ATTR_TRANSITION] @@ -114,11 +136,29 @@ async def _async_reproduce_state( if attr in state.attributes: service_data[attr] = state.attributes[attr] - for color_attr in COLOR_GROUP: - # Choose the first color that is specified - if color_attr in state.attributes: + if ( + state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN) + != COLOR_MODE_UNKNOWN + ): + # Remove deprecated white value if we got a valid color mode + service_data.pop(ATTR_WHITE_VALUE, None) + color_mode = state.attributes[ATTR_COLOR_MODE] + if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): + if color_attr not in state.attributes: + _LOGGER.warning( + "Color mode %s specified but attribute %s missing for: %s", + color_mode, + color_attr, + state.entity_id, + ) + return service_data[color_attr] = state.attributes[color_attr] - break + else: + # Fall back to Choosing the first color that is specified + for color_attr in COLOR_GROUP: + if color_attr in state.attributes: + service_data[color_attr] = state.attributes[color_attr] + break elif state.state == STATE_OFF: service = SERVICE_TURN_OFF @@ -129,11 +169,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Light states.""" await asyncio.gather( diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py index a0bd5203101..9e0f10fae47 100644 --- a/homeassistant/components/light/significant_change.py +++ b/homeassistant/components/light/significant_change.py @@ -1,5 +1,7 @@ """Helper to test significant Light state changes.""" -from typing import Any, Optional +from __future__ import annotations + +from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.significant_change import ( @@ -24,7 +26,7 @@ def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs: Any, -) -> Optional[bool]: +) -> bool | None: """Test if state significantly changed.""" if old_state != new_state: return True diff --git a/homeassistant/components/light/translations/id.json b/homeassistant/components/light/translations/id.json index bc2ba732df2..25c636ac1c7 100644 --- a/homeassistant/components/light/translations/id.json +++ b/homeassistant/components/light/translations/id.json @@ -1,8 +1,26 @@ { + "device_automation": { + "action_type": { + "brightness_decrease": "Kurangi kecerahan {entity_name}", + "brightness_increase": "Tingkatkan kecerahan {entity_name}", + "flash": "Lampu kilat {entity_name}", + "toggle": "Nyala/matikan {entity_name}", + "turn_off": "Matikan {entity_name}", + "turn_on": "Nyalakan {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala" + }, + "trigger_type": { + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan" + } + }, "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Lampu" diff --git a/homeassistant/components/light/translations/ko.json b/homeassistant/components/light/translations/ko.json index 33afda2d06d..5413ecd567e 100644 --- a/homeassistant/components/light/translations/ko.json +++ b/homeassistant/components/light/translations/ko.json @@ -1,20 +1,20 @@ { "device_automation": { "action_type": { - "brightness_decrease": "{entity_name} \uc744(\ub97c) \uc5b4\ub461\uac8c \ud558\uae30", - "brightness_increase": "{entity_name} \uc744(\ub97c) \ubc1d\uac8c \ud558\uae30", - "flash": "{entity_name} \ud50c\ub798\uc2dc", - "toggle": "{entity_name} \ud1a0\uae00", - "turn_off": "{entity_name} \ub044\uae30", - "turn_on": "{entity_name} \ucf1c\uae30" + "brightness_decrease": "{entity_name}\uc744(\ub97c) \uc5b4\ub461\uac8c \ud558\uae30", + "brightness_increase": "{entity_name}\uc744(\ub97c) \ubc1d\uac8c \ud558\uae30", + "flash": "{entity_name}\uc744(\ub97c) \uae5c\ube61\uc774\uae30", + "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30", + "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", + "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index 2144979106a..1128078f8bc 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -1,6 +1,6 @@ """Support for LightwaveRF TRV - Associated Battery.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE -from homeassistant.helpers.entity import Entity from . import CONF_SERIAL, LIGHTWAVE_LINK @@ -22,7 +22,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(batteries) -class LightwaveBattery(Entity): +class LightwaveBattery(SensorEntity): """Lightwave TRV Battery.""" def __init__(self, name, lwlink, serial): diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index bb81a022891..70a15eaf4e0 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -75,7 +75,7 @@ class LinodeBinarySensor(BinarySensorEntity): return DEVICE_CLASS_MOVING @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Linode Node.""" return self._attrs diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index c9207ec1be7..9002cb7bd11 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -67,7 +67,7 @@ class LinodeSwitch(SwitchEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Linode Node.""" return self._attrs diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index f4d4e92cb78..b7746392cee 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -5,10 +5,9 @@ import os from batinfo import Batteries import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -68,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([LinuxBatterySensor(name, battery_id, system)], True) -class LinuxBatterySensor(Entity): +class LinuxBatterySensor(SensorEntity): """Representation of a Linux Battery sensor.""" def __init__(self, name, battery_id, system): @@ -101,7 +100,7 @@ class LinuxBatterySensor(Entity): return PERCENTAGE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._system == "android": return { diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index bf939fb7535..b52cf3cf107 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,5 +1,5 @@ """Support for LIRC devices.""" -# pylint: disable=no-member, import-error +# pylint: disable=import-error import logging import threading import time diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 0c8f59c4127..f00853af524 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -59,9 +59,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = system - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -73,8 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index e1c7d8ab7b9..124b229c786 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -1,6 +1,8 @@ """Config flow for the LiteJet lighting system.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any import pylitejet from serial import SerialException @@ -18,8 +20,8 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """LiteJet config flow.""" async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """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/light.py b/homeassistant/components/litejet/light.py index 27ce904cc2c..5248afb4dbd 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -85,7 +85,7 @@ class LiteJetLight(LightEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return {ATTR_NUMBER: self._index} diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index daadfce90dc..5ae0aec9559 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -47,7 +47,7 @@ class LiteJetScene(Scene): return f"{self._entry_id}_{self._index}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index b782a4a9d98..343d8393f1c 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -77,7 +77,7 @@ class LiteJetSwitch(SwitchEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} diff --git a/homeassistant/components/litejet/translations/bg.json b/homeassistant/components/litejet/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/litejet/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/de.json b/homeassistant/components/litejet/translations/de.json index 492314e5cc6..ff528dd79b2 100644 --- a/homeassistant/components/litejet/translations/de.json +++ b/homeassistant/components/litejet/translations/de.json @@ -3,11 +3,16 @@ "abort": { "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "open_failed": "Die angegebene serielle Schnittstelle kann nicht ge\u00f6ffnet werden." + }, "step": { "user": { "data": { "port": "Port" - } + }, + "description": "Verbinde den RS232-2-Anschluss des LiteJet mit deinen Computer und gib den Pfad zum Ger\u00e4t der seriellen Schnittstelle ein.\n\nDer LiteJet MCP muss f\u00fcr 19,2 K Baud, 8 Datenbits, 1 Stoppbit, keine Parit\u00e4t und zur \u00dcbertragung eines 'CR' nach jeder Antwort konfiguriert werden.", + "title": "Verbinde zu LiteJet" } } } diff --git a/homeassistant/components/litejet/translations/hu.json b/homeassistant/components/litejet/translations/hu.json new file mode 100644 index 00000000000..6d895624c30 --- /dev/null +++ b/homeassistant/components/litejet/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "title": "Csatlakoz\u00e1s a LiteJet-hez" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/id.json b/homeassistant/components/litejet/translations/id.json new file mode 100644 index 00000000000..690692ca4cc --- /dev/null +++ b/homeassistant/components/litejet/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "open_failed": "Tidak dapat membuka port serial yang ditentukan." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Hubungkan port RS232-2 LiteJet ke komputer Anda dan masukkan jalur ke perangkat port serial.\n\nLiteJet MCP harus dikonfigurasi untuk baud 19,2 K, bit data 8,bit stop 1, tanpa paritas, dan untuk mengirimkan 'CR' setelah setiap respons.", + "title": "Hubungkan ke LiteJet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/ko.json b/homeassistant/components/litejet/translations/ko.json new file mode 100644 index 00000000000..829918dc557 --- /dev/null +++ b/homeassistant/components/litejet/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "open_failed": "\uc9c0\uc815\ud55c \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\ub97c \uc5f4 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "port": "\ud3ec\ud2b8" + }, + "description": "LiteJet\uc758 RS232-2 \ud3ec\ud2b8\ub97c \ucef4\ud4e8\ud130\uc5d0 \uc5f0\uacb0\ud558\uace0 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8 \uc7a5\uce58\uc758 \uacbd\ub85c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nLiteJet MCP\ub294 19.2 K \uc804\uc1a1\uc18d\ub3c4, 8 \ub370\uc774\ud130 \ube44\ud2b8, 1 \uc815\uc9c0 \ube44\ud2b8, \ud328\ub9ac\ud2f0 \uc5c6\uc74c\uc73c\ub85c \uad6c\uc131\ub418\uc5b4\uc57c \ud558\uba70 \uac01 \uc751\ub2f5 \ud6c4\uc5d0 'CR'\uc744 \uc804\uc1a1\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "Litejet\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/nl.json b/homeassistant/components/litejet/translations/nl.json index f16f25a3987..a96f8de6171 100644 --- a/homeassistant/components/litejet/translations/nl.json +++ b/homeassistant/components/litejet/translations/nl.json @@ -11,6 +11,7 @@ "data": { "port": "Poort" }, + "description": "Verbind de RS232-2 poort van de LiteJet met uw computer en voer het pad naar het seri\u00eble poortapparaat in.\n\nDe LiteJet MCP moet worden geconfigureerd voor 19,2 K baud, 8 databits, 1 stopbit, geen pariteit, en om een 'CR' uit te zenden na elk antwoord.", "title": "Maak verbinding met LiteJet" } } diff --git a/homeassistant/components/litejet/translations/pt.json b/homeassistant/components/litejet/translations/pt.json new file mode 100644 index 00000000000..8b09f9a245f --- /dev/null +++ b/homeassistant/components/litejet/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 71841d9c4fd..6800282766b 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -31,6 +31,7 @@ TRIGGER_SCHEMA = vol.Schema( async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) held_less_than = config.get(CONF_HELD_LESS_THAN) @@ -50,6 +51,7 @@ async def async_attach_trigger(hass, config, action, automation_info): CONF_HELD_MORE_THAN: held_more_than, CONF_HELD_LESS_THAN: held_less_than, "description": f"litejet switch #{number}", + "id": trigger_id, } }, ) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 19e76b9bb19..84e6822dc13 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -30,9 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except LitterRobotException as ex: raise ConfigEntryNotReady from ex - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -43,8 +43,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 36fc2064abb..67b73ee12d5 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN from .hub import LitterRobotHub _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 943ef5bfe37..86c3aff5462 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,8 +1,10 @@ """A wrapper 'hub' for the Litter-Robot API and base entity for common attributes.""" +from __future__ import annotations + from datetime import time, timedelta import logging from types import MethodType -from typing import Any, Optional +from typing import Any import pylitterbot from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -106,16 +108,19 @@ class LitterRobotEntity(CoordinatorEntity): async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback) @staticmethod - def parse_time_at_default_timezone(time_str: str) -> Optional[time]: + 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 time( - hour=parsed_time.hour, - minute=parsed_time.minute, - second=parsed_time.second, - tzinfo=dt_util.DEFAULT_TIME_ZONE, + 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/sensor.py b/homeassistant/components/litterrobot/sensor.py index 8900c6c54ca..8038fdbb2cb 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,16 +1,16 @@ """Support for Litter-Robot sensors.""" -from typing import Optional +from __future__ import annotations from pylitterbot.robot import Robot +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE -from homeassistant.helpers.entity import Entity from .const import DOMAIN from .hub import LitterRobotEntity, LitterRobotHub -def icon_for_gauge_level(gauge_level: Optional[int] = None, offset: int = 0) -> str: +def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str: """Return a gauge icon valid identifier.""" if gauge_level is None or gauge_level <= 0 + offset: return "mdi:gauge-empty" @@ -21,7 +21,7 @@ def icon_for_gauge_level(gauge_level: Optional[int] = None, offset: int = 0) -> return "mdi:gauge-low" -class LitterRobotPropertySensor(LitterRobotEntity, Entity): +class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): """Litter-Robot property sensors.""" def __init__( @@ -37,7 +37,7 @@ class LitterRobotPropertySensor(LitterRobotEntity, Entity): return getattr(self.robot, self.sensor_attribute) -class LitterRobotWasteSensor(LitterRobotPropertySensor, Entity): +class LitterRobotWasteSensor(LitterRobotPropertySensor): """Litter-Robot sensors.""" @property @@ -51,7 +51,7 @@ class LitterRobotWasteSensor(LitterRobotPropertySensor, Entity): return icon_for_gauge_level(self.state, 10) -class LitterRobotSleepTimeSensor(LitterRobotPropertySensor, Entity): +class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): """Litter-Robot sleep time sensors.""" @property diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json new file mode 100644 index 00000000000..67a484573aa --- /dev/null +++ b/homeassistant/components/litterrobot/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json new file mode 100644 index 00000000000..12a48f17c32 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Fallo al conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/hu.json b/homeassistant/components/litterrobot/translations/hu.json new file mode 100644 index 00000000000..fd8db27da5e --- /dev/null +++ b/homeassistant/components/litterrobot/translations/hu.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/id.json b/homeassistant/components/litterrobot/translations/id.json new file mode 100644 index 00000000000..4a84db42a14 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/id.json @@ -0,0 +1,20 @@ +{ + "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": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/ko.json b/homeassistant/components/litterrobot/translations/ko.json new file mode 100644 index 00000000000..94261de9637 --- /dev/null +++ b/homeassistant/components/litterrobot/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": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pt.json b/homeassistant/components/litterrobot/translations/pt.json new file mode 100644 index 00000000000..7953cf5625c --- /dev/null +++ b/homeassistant/components/litterrobot/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json index 3f4677a050e..aef0fdff54e 100644 --- a/homeassistant/components/litterrobot/translations/ru.json +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 4fe76d446f4..a36ef656361 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -111,7 +111,7 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity): raise NotImplementedError() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return { "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index b0b84677183..c94aeff24b0 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -105,6 +105,6 @@ class LocalFile(Camera): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the camera state attributes.""" return {"file_path": self._file_path} diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index d159b641fa2..1d2cce72105 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -1,7 +1,7 @@ """Sensor platform for local_ip.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME -from homeassistant.helpers.entity import Entity from homeassistant.util import get_local_ip from .const import DOMAIN, SENSOR @@ -13,7 +13,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([IPSensor(name)], True) -class IPSensor(Entity): +class IPSensor(SensorEntity): """A simple sensor.""" def __init__(self, name): diff --git a/homeassistant/components/local_ip/translations/de.json b/homeassistant/components/local_ip/translations/de.json index 9e2a6eda5c6..2d31f90139d 100644 --- a/homeassistant/components/local_ip/translations/de.json +++ b/homeassistant/components/local_ip/translations/de.json @@ -8,7 +8,7 @@ "data": { "name": "Sensorname" }, - "description": "M\u00f6chtest du mit der Einrichtung beginnen?", + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", "title": "Lokale IP-Adresse" } } diff --git a/homeassistant/components/local_ip/translations/hu.json b/homeassistant/components/local_ip/translations/hu.json index 09b894742a6..b692e87f922 100644 --- a/homeassistant/components/local_ip/translations/hu.json +++ b/homeassistant/components/local_ip/translations/hu.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "step": { "user": { "data": { "name": "\u00c9rz\u00e9kel\u0151 neve" }, + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "Helyi IP c\u00edm" } } diff --git a/homeassistant/components/local_ip/translations/id.json b/homeassistant/components/local_ip/translations/id.json new file mode 100644 index 00000000000..a7d8993baa6 --- /dev/null +++ b/homeassistant/components/local_ip/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "data": { + "name": "Nama Sensor" + }, + "description": "Ingin memulai penyiapan?", + "title": "Alamat IP Lokal" + } + } + }, + "title": "Alamat IP Lokal" +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/ko.json b/homeassistant/components/local_ip/translations/ko.json index 3b543f87f79..f7248ab0f6f 100644 --- a/homeassistant/components/local_ip/translations/ko.json +++ b/homeassistant/components/local_ip/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "user": { diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index 57547adedd8..b8b033d2e73 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van lokaal IP toegestaan." + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "user": { diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 28af822ae63..bb2a19c6380 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -1,6 +1,7 @@ """Support for Locative.""" +from __future__ import annotations + import logging -from typing import Dict from aiohttp import web import voluptuous as vol @@ -34,7 +35,7 @@ def _id(value: str) -> str: return value.replace("-", "") -def _validate_test_mode(obj: Dict) -> Dict: +def _validate_test_mode(obj: dict) -> dict: """Validate that id is provided outside of test mode.""" if ATTR_ID not in obj and obj[ATTR_TRIGGER] != "test": raise vol.Invalid("Location id not specified") diff --git a/homeassistant/components/locative/translations/de.json b/homeassistant/components/locative/translations/de.json index a6dcf4150d0..2df9f889e85 100644 --- a/homeassistant/components/locative/translations/de.json +++ b/homeassistant/components/locative/translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?", + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", "title": "Locative Webhook einrichten" } } diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json index 983ffacfa7d..8dc03e9c37a 100644 --- a/homeassistant/components/locative/translations/hu.json +++ b/homeassistant/components/locative/translations/hu.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?", + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/locative/translations/id.json b/homeassistant/components/locative/translations/id.json new file mode 100644 index 00000000000..71aea5cc63c --- /dev/null +++ b/homeassistant/components/locative/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim lokasi ke Home Assistant, Anda harus mengatur fitur webhook di aplikasi Locative.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) untuk detail lebih lanjut." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?", + "title": "Siapkan Locative Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/ko.json b/homeassistant/components/locative/translations/ko.json index 5930e7edf1b..50652f76fc5 100644 --- a/homeassistant/components/locative/translations/ko.json +++ b/homeassistant/components/locative/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative \uc571\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Locative\uc571\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/nl.json b/homeassistant/components/locative/translations/nl.json index 16cbbc77277..d66a1262b5d 100644 --- a/homeassistant/components/locative/translations/nl.json +++ b/homeassistant/components/locative/translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Weet u zeker dat u de Locative Webhook wilt instellen?", + "description": "Wil je beginnen met instellen?", "title": "Stel de Locative Webhook in" } } diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index bc7dc10ba8d..237daedae80 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import final import voluptuous as vol @@ -76,7 +77,7 @@ async def async_unload_entry(hass, entry): class LockEntity(Entity): - """Representation of a lock.""" + """Base class for lock entities.""" @property def changed_by(self): @@ -117,6 +118,7 @@ class LockEntity(Entity): """Open the door latch.""" await self.hass.async_add_executor_job(ft.partial(self.open, **kwargs)) + @final @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index efdb5e352cf..cb0e2b0daad 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Lock.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -30,7 +30,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Lock devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -75,11 +75,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if config[CONF_TYPE] == "lock": diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index a25018dc709..0fae680f829 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -1,5 +1,5 @@ """Provides device automations for Lock.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -30,7 +30,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device conditions for Lock devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 05d5041ca65..77eb04e3735 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Lock.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, + CONF_FOR, CONF_PLATFORM, CONF_TYPE, STATE_LOCKED, @@ -27,11 +28,12 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Lock devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -42,28 +44,29 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: continue # Add triggers for each entity that belongs to this integration - triggers.append( + triggers += [ { CONF_PLATFORM: "device", CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "locked", + CONF_TYPE: trigger, } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "unlocked", - } - ) + for trigger in TRIGGER_TYPES + ] return triggers +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -71,8 +74,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] == "locked": to_state = STATE_LOCKED else: @@ -83,6 +84,8 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_trigger.CONF_TO: to_state, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] state_config = state_trigger.TRIGGER_SCHEMA(state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index 812b9bf04df..0d575964b2b 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Lock state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -24,8 +26,8 @@ async def _async_reproduce_state( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -60,8 +62,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Lock states.""" await asyncio.gather( diff --git a/homeassistant/components/lock/significant_change.py b/homeassistant/components/lock/significant_change.py index 59a3b1a95c5..172bf2559c5 100644 --- a/homeassistant/components/lock/significant_change.py +++ b/homeassistant/components/lock/significant_change.py @@ -1,5 +1,7 @@ """Helper to test significant Lock state changes.""" -from typing import Any, Optional +from __future__ import annotations + +from typing import Any from homeassistant.core import HomeAssistant, callback @@ -12,7 +14,7 @@ def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs: Any, -) -> Optional[bool]: +) -> bool | None: """Test if state significantly changed.""" if old_state != new_state: return True diff --git a/homeassistant/components/lock/translations/id.json b/homeassistant/components/lock/translations/id.json index da11e3422f1..d8778868651 100644 --- a/homeassistant/components/lock/translations/id.json +++ b/homeassistant/components/lock/translations/id.json @@ -1,8 +1,23 @@ { + "device_automation": { + "action_type": { + "lock": "Kunci {entity_name}", + "open": "Buka {entity_name}", + "unlock": "Buka kunci {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} terkunci", + "is_unlocked": "{entity_name} tidak terkunci" + }, + "trigger_type": { + "locked": "{entity_name} terkunci", + "unlocked": "{entity_name} tidak terkunci" + } + }, "state": { "_": { "locked": "Terkunci", - "unlocked": "Terbuka" + "unlocked": "Tidak Terkunci" } }, "title": "Kunci" diff --git a/homeassistant/components/lock/translations/ko.json b/homeassistant/components/lock/translations/ko.json index 6f04e2b4110..b8db32ad513 100644 --- a/homeassistant/components/lock/translations/ko.json +++ b/homeassistant/components/lock/translations/ko.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "lock": "{entity_name} \uc7a0\uae08", - "open": "{entity_name} \uc5f4\uae30", - "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c" + "lock": "{entity_name}\uc744(\ub97c) \uc7a0\uadf8\uae30", + "open": "{entity_name}\uc744(\ub97c) \uc5f4\uae30", + "unlock": "{entity_name}\uc744(\ub97c) \uc7a0\uae08 \ud574\uc81c\ud558\uae30" }, "condition_type": { - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", - "is_unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74" + "is_locked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_unlocked": "{entity_name}\uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74" }, "trigger_type": { - "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", - "unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c" + "locked": "{entity_name}\uc774(\uac00) \uc7a0\uacbc\uc744 \ub54c", + "unlocked": "{entity_name}\uc774(\uac00) \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc744 \ub54c" } }, "state": { @@ -20,5 +20,5 @@ "unlocked": "\ud574\uc81c" } }, - "title": "\uc7a0\uae40" + "title": "\uc7a0\uae08\uc7a5\uce58" } \ No newline at end of file diff --git a/homeassistant/components/lock/translations/zh-Hans.json b/homeassistant/components/lock/translations/zh-Hans.json index 4999c52f8e0..7e2a7719718 100644 --- a/homeassistant/components/lock/translations/zh-Hans.json +++ b/homeassistant/components/lock/translations/zh-Hans.json @@ -1,13 +1,13 @@ { "device_automation": { "action_type": { - "lock": "\u4e0a\u9501{entity_name}", - "open": "\u5f00\u542f{entity_name}", - "unlock": "\u89e3\u9501{entity_name}" + "lock": "\u9501\u5b9a {entity_name}", + "open": "\u5f00\u542f {entity_name}", + "unlock": "\u89e3\u9501 {entity_name}" }, "condition_type": { - "is_locked": "{entity_name}\u5df2\u4e0a\u9501", - "is_unlocked": "{entity_name}\u5df2\u89e3\u9501" + "is_locked": "{entity_name} \u5df2\u9501\u5b9a", + "is_unlocked": "{entity_name} \u5df2\u89e3\u9501" }, "trigger_type": { "locked": "{entity_name} \u88ab\u9501\u5b9a", diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 3b77e6e6409..8d216b5c6f0 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" +from contextlib import suppress from datetime import timedelta from itertools import groupby import json @@ -231,6 +232,12 @@ class LogbookView(HomeAssistantView): hass = request.app["hass"] entity_matches_only = "entity_matches_only" in request.query + context_id = request.query.get("context_id") + + if entity_ids and context_id: + return self.json_message( + "Can't combine entity with context_id", HTTP_BAD_REQUEST + ) def json_events(): """Fetch events and generate JSON.""" @@ -243,6 +250,7 @@ class LogbookView(HomeAssistantView): self.filters, self.entities_filter, entity_matches_only, + context_id, ) ) @@ -377,10 +385,8 @@ def humanify(hass, events, entity_attr_cache, context_lookup): domain = event_data.get(ATTR_DOMAIN) entity_id = event_data.get(ATTR_ENTITY_ID) if domain is None and entity_id is not None: - try: + with suppress(IndexError): domain = split_entity_id(str(entity_id))[0] - except IndexError: - pass data = { "when": event.time_fired_isoformat, @@ -413,8 +419,13 @@ def _get_events( filters=None, entities_filter=None, entity_matches_only=False, + context_id=None, ): """Get events for a period of time.""" + assert not ( + entity_ids and context_id + ), "can't pass in both entity_ids and context_id" + entity_attr_cache = EntityAttributeCache(hass) context_lookup = {None: None} @@ -466,6 +477,9 @@ def _get_events( filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED) ) + if context_id is not None: + query = query.filter(Events.context_id == context_id) + query = query.order_by(Events.time_fired) return list( diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index ba0026fedc5..fb2920fb6e2 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -26,6 +26,7 @@ DEFAULT_LOGSEVERITY = "DEBUG" LOGGER_DEFAULT = "default" LOGGER_LOGS = "logs" +LOGGER_FILTERS = "filters" ATTR_LEVEL = "level" @@ -40,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, vol.Optional(LOGGER_LOGS): vol.Schema({cv.string: _VALID_LOG_LEVEL}), + vol.Optional(LOGGER_FILTERS): vol.Schema({cv.string: [cv.is_regex]}), } ) }, @@ -70,6 +72,11 @@ async def async_setup(hass, config): if LOGGER_LOGS in config[DOMAIN]: set_log_levels(config[DOMAIN][LOGGER_LOGS]) + if LOGGER_FILTERS in config[DOMAIN]: + for key, value in config[DOMAIN][LOGGER_FILTERS].items(): + logger = logging.getLogger(key) + _add_log_filter(logger, value) + @callback def async_service_handler(service): """Handle logger services.""" @@ -103,6 +110,15 @@ def _set_log_level(logger, level): getattr(logger, "orig_setLevel", logger.setLevel)(LOGSEVERITY[level]) +def _add_log_filter(logger, patterns): + """Add a Filter to the logger based on a regexp of the filter_str.""" + + def filter_func(logrecord): + return not any(p.match(logrecord.getMessage()) for p in patterns) + + logger.addFilter(filter_func) + + def _get_logger_class(hass_overrides): """Create a logger subclass. diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 056783ef6da..3364cd725c7 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -47,6 +47,8 @@ SERVICE_LIVESTREAM_RECORD = "livestream_record" ATTR_VALUE = "value" ATTR_DURATION = "duration" +PLATFORMS = ["camera", "sensor"] + SENSOR_SCHEMA = vol.Schema( { vol.Optional(CONF_MONITORED_CONDITIONS, default=list(LOGI_SENSORS)): vol.All( @@ -171,9 +173,9 @@ async def async_setup_entry(hass, entry): hass.data[DATA_LOGI] = logi_circle - for component in "camera", "sensor": + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) async def service_handler(service): @@ -219,8 +221,8 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - for component in "camera", "sensor": - await hass.config_entries.async_forward_entry_unload(entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, platform) logi_circle = hass.data.pop(DATA_LOGI) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 20bc829d75d..1afeb190c8b 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -125,7 +125,7 @@ class LogiCam(Camera): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state = { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 4a5fedaf57a..29cd6e28e1c 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -1,6 +1,7 @@ """Support for Logi Circle sensors.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, @@ -9,7 +10,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.dt import as_local @@ -42,7 +42,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors, True) -class LogiSensor(Entity): +class LogiSensor(SensorEntity): """A sensor implementation for a Logi Circle camera.""" def __init__(self, camera, time_zone, sensor_type): @@ -83,7 +83,7 @@ class LogiSensor(Entity): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state = { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 04c8229f5ff..9c788350de4 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + }, "error": { - "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot" + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "user": { diff --git a/homeassistant/components/logi_circle/translations/id.json b/homeassistant/components/logi_circle/translations/id.json new file mode 100644 index 00000000000..3cbfdd03978 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "external_error": "Eksepsi terjadi dari alur lain.", + "external_setup": "Logi Circle berhasil dikonfigurasi dari alur lain.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi." + }, + "error": { + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "follow_link": "Ikuti tautan dan autentikasi sebelum menekan Kirim.", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "auth": { + "description": "Buka tautan di bawah ini dan **Terima** akses ke akun Logi Circle Anda, lalu kembali dan tekan **Kirim** di bawah ini.\n\n[Tautan] ({authorization_url})", + "title": "Autentikasi dengan Logi Circle" + }, + "user": { + "data": { + "flow_impl": "Penyedia" + }, + "description": "Pilih melalui penyedia autentikasi mana yang ingin Anda autentikasi dengan Logi Circle.", + "title": "Penyedia Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json index 2300bbb27c6..733c95f95e0 100644 --- a/homeassistant/components/logi_circle/translations/ko.json +++ b/homeassistant/components/logi_circle/translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "external_error": "\ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "external_setup": "Logi Circle \uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "external_setup": "Logi Circle\uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\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." }, "error": { diff --git a/homeassistant/components/logi_circle/translations/nl.json b/homeassistant/components/logi_circle/translations/nl.json index 36970feb48b..8c4d81d120e 100644 --- a/homeassistant/components/logi_circle/translations/nl.json +++ b/homeassistant/components/logi_circle/translations/nl.json @@ -13,7 +13,7 @@ }, "step": { "auth": { - "description": "Volg de onderstaande link en Accepteer toegang tot uw Logi Circle-account, kom dan terug en druk hieronder op Verzenden . \n\n [Link] ({authorization_url})", + "description": "Volg de onderstaande link en **Accepteer** toegang tot uw Logi Circle-account, kom dan terug en druk hieronder op **Verzenden** . \n\n [Link] ({authorization_url})", "title": "Authenticeren met Logi Circle" }, "user": { diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index a8bebc20cf5..23eea5c00e0 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -5,10 +5,9 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import HTTP_OK import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -90,7 +89,7 @@ class APIData: self.data = parse_api_response(response.json()) -class AirSensor(Entity): +class AirSensor(SensorEntity): """Single authority air sensor.""" ICON = "mdi:cloud-outline" @@ -124,7 +123,7 @@ class AirSensor(Entity): return self.ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" attrs = {} attrs["updated"] = self._updated diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index c39ef2885b0..b7f2cb50cbf 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -4,10 +4,9 @@ from datetime import timedelta from london_tube_status import TubeData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTRIBUTION = "Powered by TfL Open Data" @@ -51,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class LondonTubeSensor(Entity): +class LondonTubeSensor(SensorEntity): """Sensor that reads the status of a line from Tube Data.""" def __init__(self, name, data): @@ -78,7 +77,7 @@ class LondonTubeSensor(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" self.attrs["Description"] = self._description return self.attrs diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 85494d354b5..78e55f22eb8 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -4,14 +4,13 @@ import logging import pyloopenergy import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -82,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class LoopEnergyDevice(Entity): +class LoopEnergySensor(SensorEntity): """Implementation of an Loop Energy base sensor.""" def __init__(self, controller): @@ -116,7 +115,7 @@ class LoopEnergyDevice(Entity): self.schedule_update_ha_state(True) -class LoopEnergyElec(LoopEnergyDevice): +class LoopEnergyElec(LoopEnergySensor): """Implementation of an Loop Energy Electricity sensor.""" def __init__(self, controller): @@ -133,7 +132,7 @@ class LoopEnergyElec(LoopEnergyDevice): self._state = round(self._controller.electricity_useage, 2) -class LoopEnergyGas(LoopEnergyDevice): +class LoopEnergyGas(LoopEnergySensor): """Implementation of an Loop Energy Gas sensor.""" def __init__(self, controller): diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index e673b2a470b..45011239f16 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -34,7 +34,7 @@ from .const import ( STORAGE_DASHBOARD_UPDATE_FIELDS, url_slug, ) -from .system_health import system_health_info # NOQA +from .system_health import system_health_info # noqa: F401 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 2d3196054e3..93b127259d2 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -1,7 +1,10 @@ """Lovelace dashboard support.""" +from __future__ import annotations + from abc import ABC, abstractmethod import logging import os +from pathlib import Path import time from typing import Optional, cast @@ -12,7 +15,7 @@ from homeassistant.const import CONF_FILENAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage -from homeassistant.util.yaml import load_yaml +from homeassistant.util.yaml import Secrets, load_yaml from .const import ( CONF_ICON, @@ -201,7 +204,7 @@ class LovelaceYAML(LovelaceConfig): is_updated = self._cache is not None try: - config = load_yaml(self.path) + config = load_yaml(self.path, Secrets(Path(self.hass.config.config_dir))) except FileNotFoundError: raise ConfigNotFound from None @@ -230,7 +233,7 @@ class DashboardsCollection(collection.StorageCollection): _LOGGER, ) - async def _async_load_data(self) -> Optional[dict]: + async def _async_load_data(self) -> dict | None: """Load the data.""" data = await self.store.async_load() diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 0a3e36892d5..6a97d5c4192 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -1,6 +1,8 @@ """Lovelace resources support.""" +from __future__ import annotations + import logging -from typing import List, Optional, cast +from typing import Optional, cast import uuid import voluptuous as vol @@ -38,7 +40,7 @@ class ResourceYAMLCollection: return {"resources": len(self.async_items() or [])} @callback - def async_items(self) -> List[dict]: + def async_items(self) -> list[dict]: """Return list of items in collection.""" return self.data @@ -66,7 +68,7 @@ class ResourceStorageCollection(collection.StorageCollection): return {"resources": len(self.async_items() or [])} - async def _async_load_data(self) -> Optional[dict]: + async def _async_load_data(self) -> dict | None: """Load the data.""" data = await self.store.async_load() diff --git a/homeassistant/components/lovelace/translations/id.json b/homeassistant/components/lovelace/translations/id.json new file mode 100644 index 00000000000..d945bc04f22 --- /dev/null +++ b/homeassistant/components/lovelace/translations/id.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Dasbor", + "mode": "Mode", + "resources": "Sumber Daya", + "views": "Tampilan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/ko.json b/homeassistant/components/lovelace/translations/ko.json new file mode 100644 index 00000000000..48a26a1371c --- /dev/null +++ b/homeassistant/components/lovelace/translations/ko.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "\ub300\uc2dc\ubcf4\ub4dc", + "mode": "\ubaa8\ub4dc", + "resources": "\ub9ac\uc18c\uc2a4", + "views": "\ubdf0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 3b51aab6e4a..95fd6fc35ad 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -2,6 +2,6 @@ "domain": "luci", "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", - "requirements": ["openwrt-luci-rpc==1.1.6"], + "requirements": ["openwrt-luci-rpc==1.1.8"], "codeowners": ["@mzdrale"] } diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 515d8ad577f..aec77961b94 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -1,6 +1,7 @@ """Support for Luftdaten sensors.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -9,7 +10,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import ( DATA_LUFTDATEN, @@ -45,7 +45,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors, True) -class LuftdatenSensor(Entity): +class LuftdatenSensor(SensorEntity): """Implementation of a Luftdaten sensor.""" def __init__(self, luftdaten, sensor_type, name, icon, unit, show): @@ -94,7 +94,7 @@ class LuftdatenSensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION diff --git a/homeassistant/components/luftdaten/translations/hu.json b/homeassistant/components/luftdaten/translations/hu.json index 4385c09d6ef..2fa90c23ca8 100644 --- a/homeassistant/components/luftdaten/translations/hu.json +++ b/homeassistant/components/luftdaten/translations/hu.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_sensor": "Az \u00e9rz\u00e9kel\u0151 nem el\u00e9rhet\u0151 vagy \u00e9rv\u00e9nytelen" }, "step": { diff --git a/homeassistant/components/luftdaten/translations/id.json b/homeassistant/components/luftdaten/translations/id.json new file mode 100644 index 00000000000..96ec6d5f20f --- /dev/null +++ b/homeassistant/components/luftdaten/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Layanan sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_sensor": "Sensor tidak tersedia atau tidak valid" + }, + "step": { + "user": { + "data": { + "show_on_map": "Tampilkan di peta", + "station_id": "ID Sensor Luftdaten" + }, + "title": "Konfigurasikan Luftdaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 2eeebac4c96..b38968c36b8 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -12,6 +12,8 @@ from homeassistant.util import slugify DOMAIN = "lutron" +PLATFORMS = ["light", "cover", "switch", "scene", "binary_sensor"] + _LOGGER = logging.getLogger(__name__) LUTRON_BUTTONS = "lutron_buttons" @@ -37,7 +39,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, base_config): - """Set up the Lutron component.""" + """Set up the Lutron integration.""" hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None hass.data[LUTRON_DEVICES] = { @@ -92,8 +94,8 @@ def setup(hass, base_config): (area.name, area.occupancy_group) ) - for component in ("light", "cover", "switch", "scene", "binary_sensor"): - discovery.load_platform(hass, component, DOMAIN, {}, base_config) + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, base_config) return True diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index f77a2b120da..6fb394d333c 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -49,6 +49,6 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity): return f"{self._area_name} Occupancy" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"lutron_integration_id": self._lutron_device.id} diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index f1faed32161..6ee53950ef2 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -64,6 +64,6 @@ class LutronCover(LutronDevice, CoverEntity): _LOGGER.debug("Lutron ID: %d updated to %f", self._lutron_device.id, level) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"lutron_integration_id": self._lutron_device.id} diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index d74d24f71a1..de94b6d6ead 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -65,7 +65,7 @@ class LutronLight(LutronDevice, LightEntity): self._lutron_device.level = 0 @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"lutron_integration_id": self._lutron_device.id} diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 21586aaa266..f78f46b6733 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -42,7 +42,7 @@ class LutronSwitch(LutronDevice, SwitchEntity): self._lutron_device.level = 0 @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"lutron_integration_id": self._lutron_device.id} @@ -75,7 +75,7 @@ class LutronLed(LutronDevice, SwitchEntity): self._lutron_device.state = 0 @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { "keypad": self._keypad_name, diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 56cc7a78c96..89eef781c25 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -62,12 +62,11 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_sensor"] +PLATFORMS = ["light", "switch", "cover", "scene", "fan", "binary_sensor"] async def async_setup(hass, base_config): """Set up the Lutron component.""" - hass.data.setdefault(DOMAIN, {}) if DOMAIN in base_config: @@ -92,7 +91,6 @@ async def async_setup(hass, base_config): async def async_setup_entry(hass, config_entry): """Set up a bridge from a config entry.""" - host = config_entry.data[CONF_HOST] keyfile = hass.config.path(config_entry.data[CONF_KEYFILE]) certfile = hass.config.path(config_entry.data[CONF_CERTFILE]) @@ -125,7 +123,7 @@ async def async_setup_entry(hass, config_entry): bridge_device = devices[BRIDGE_DEVICE_ID] await _async_register_bridge_device(hass, config_entry.entry_id, bridge_device) # Store this bridge (keyed by entry_id) so it can be retrieved by the - # components we're setting up. + # platforms we're setting up. hass.data[DOMAIN][config_entry.entry_id] = { BRIDGE_LEAP: bridge, BRIDGE_DEVICE: bridge_device, @@ -139,9 +137,9 @@ 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 component in LUTRON_CASETA_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -280,7 +278,6 @@ def _async_subscribe_pico_remote_events(hass, lip, button_devices_by_id): async def async_unload_entry(hass, config_entry): """Unload the bridge bridge from a config entry.""" - data = hass.data[DOMAIN][config_entry.entry_id] data[BRIDGE_LEAP].close() if data[BRIDGE_LIP]: @@ -289,8 +286,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in LUTRON_CASETA_COMPONENTS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -352,7 +349,7 @@ class LutronCasetaDevice(Entity): } @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"device_id": self.device_id, "zone_id": self._device["zone"]} diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 97053eba08c..c2fc311de43 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -16,7 +16,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Adds occupancy groups from the Caseta bridge associated with the config_entry as binary_sensor entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] @@ -70,6 +69,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"device_id": self.device_id} diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 6cd30a78f0c..f591369b570 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -10,7 +10,6 @@ from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.zeroconf import ATTR_HOSTNAME from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback @@ -21,10 +20,10 @@ from .const import ( CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, + DOMAIN, ERROR_CANNOT_CONNECT, STEP_IMPORT_FAILED, ) -from .const import DOMAIN # pylint: disable=unused-import HOSTNAME = "hostname" @@ -66,7 +65,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, discovery_info): """Handle a flow initialized by zeroconf discovery.""" - hostname = discovery_info[ATTR_HOSTNAME] + hostname = discovery_info["hostname"] if hostname is None or not hostname.startswith("lutron-"): return self.async_abort(reason="not_lutron_device") @@ -171,7 +170,6 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by `async_setup`. """ - host = import_info[CONF_HOST] # Store the imported config for other steps in this flow to access. self.data[CONF_HOST] = host @@ -213,7 +211,6 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_validate_connectable_bridge_config(self): """Check if we can connect to the bridge with the current config.""" - bridge = None try: diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index fcc647f00ba..40a5d2b01fd 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -31,7 +31,6 @@ ATTR_ACTION = "action" ACTION_PRESS = "press" ACTION_RELEASE = "release" -CONF_TYPE = "type" CONF_SUBTYPE = "subtype" BRIDGE_TIMEOUT = 35 diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index b3924ba31c8..31f7e9b55bd 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -24,7 +24,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Adds shades from the Caseta bridge associated with the config_entry as cover entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 86ee5e46b51..230301c12f2 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -1,5 +1,5 @@ """Provides device triggers for lutron caseta.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -225,7 +225,7 @@ async def async_validate_trigger_config(hass: HomeAssistant, config: ConfigType) return schema(config) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for lutron caseta devices.""" triggers = [] diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index edda379aedc..edca88c10fc 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -1,4 +1,6 @@ """Support for Lutron Caseta fans.""" +from __future__ import annotations + import logging from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF @@ -24,7 +26,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Adds fan controllers from the Caseta bridge associated with the config_entry as fan entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] @@ -42,7 +43,7 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): """Representation of a Lutron Caseta fan. Including Fan Speed.""" @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed percentage.""" if self._device["fan_speed"] is None: return None diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index ec200118082..016dd925b23 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -33,7 +33,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Adds dimmers from the Caseta bridge associated with the config_entry as light entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index d70048db8cd..43e0429d151 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -12,7 +12,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Adds scenes from the Caseta bridge associated with the config_entry as scene entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json index 604a3c24ab2..9464523fcce 100644 --- a/homeassistant/components/lutron_caseta/strings.json +++ b/homeassistant/components/lutron_caseta/strings.json @@ -7,7 +7,7 @@ "description": "Couldn’t setup bridge (host: {host}) imported from configuration.yaml." }, "user": { - "title": "Automaticlly connect to the bridge", + "title": "Automatically connect to the bridge", "description": "Enter the IP address of the device.", "data": { "host": "[%key:common::config_flow::data::host%]" diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 1e5b4ab6fe5..c6aea447055 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -15,7 +15,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Adds switches from the Caseta bridge associated with the config_entry as switch entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] diff --git a/homeassistant/components/lutron_caseta/translations/bg.json b/homeassistant/components/lutron_caseta/translations/bg.json new file mode 100644 index 00000000000..ba9f144cb0a --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/bg.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "off": "\u0418\u0437\u043a\u043b.", + "on": "\u0412\u043a\u043b." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index b6aacf2d0ef..392648136bd 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -2,17 +2,35 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "not_lutron_device": "Erkanntes Ger\u00e4t ist kein Lutron-Ger\u00e4t" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { + "link": { + "title": "Mit der Bridge verbinden" + }, "user": { "data": { "host": "Host" - } + }, + "description": "Gib die IP-Adresse des Ger\u00e4ts ein.", + "title": "Automatisch mit der Bridge verbinden" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", + "off": "Aus", + "on": "An", + "stop_all": "Alle anhalten" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index 96c00d6cb42..2088cd52327 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -23,7 +23,7 @@ "host": "Host" }, "description": "Enter the IP address of the device.", - "title": "Automaticlly connect to the bridge" + "title": "Automatically connect to the bridge" } } }, diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json index 81fee6d5b4a..5e57dd63b8b 100644 --- a/homeassistant/components/lutron_caseta/translations/et.json +++ b/homeassistant/components/lutron_caseta/translations/et.json @@ -23,7 +23,7 @@ "host": "" }, "description": "Sisesta seadme IP-aadress.", - "title": "\u00dchendu sillaga automaatselt" + "title": "\u00dchenda sillaga automaatselt" } } }, diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json new file mode 100644 index 00000000000..7b55b0743fb --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/he.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4- IP \u05e9\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json new file mode 100644 index 00000000000..921f5e83409 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + }, + "description": "Add meg az eszk\u00f6z IP-c\u00edm\u00e9t." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "off": "Ki", + "on": "Be", + "stop_all": "Az \u00f6sszes le\u00e1ll\u00edt\u00e1sa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/id.json b/homeassistant/components/lutron_caseta/translations/id.json new file mode 100644 index 00000000000..b14e9ad1c23 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/id.json @@ -0,0 +1,70 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "not_lutron_device": "Perangkat yang ditemukan bukan perangkat Lutron" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "step": { + "import_failed": { + "description": "Tidak dapat menyiapkan bridge (host: {host} ) yang diimpor dari configuration.yaml.", + "title": "Gagal mengimpor konfigurasi bridge Cas\u00e9ta." + }, + "link": { + "description": "Untuk memasangkan dengan {name} ({host}), setelah mengirimkan formulir ini, tekan tombol hitam di bagian belakang bridge.", + "title": "Pasangkan dengan bridge" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Masukkan alamat IP perangkat.", + "title": "Sambungkan secara otomatis ke bridge" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Tombol pertama", + "button_2": "Tombol kedua", + "button_3": "Tombol ketiga", + "button_4": "Tombol keempat", + "close_1": "Tutup 1", + "close_2": "Tutup 2", + "close_3": "Tutup 3", + "close_4": "Tutup 4", + "close_all": "Tutup semua", + "group_1_button_1": "Tombol pertama Grup Pertama", + "group_1_button_2": "Tombol kedua Grup Pertama", + "group_2_button_1": "Tombol pertama Grup Kedua", + "group_2_button_2": "Tombol kedua Grup Kedua", + "lower": "Rendah", + "lower_1": "Rendah 1", + "lower_2": "Rendah 2", + "lower_3": "Rendah 3", + "lower_4": "Rendah 4", + "lower_all": "Rendahkan semua", + "off": "Mati", + "on": "Nyala", + "open_1": "Buka 1", + "open_2": "Buka 2", + "open_3": "Buka 3", + "open_4": "Buka 4", + "open_all": "Buka semua", + "stop": "Hentikan (favorit)", + "stop_1": "Hentikan 1", + "stop_2": "Hentikan 2", + "stop_3": "Hentikan 3", + "stop_4": "Hentikan 4", + "stop_all": "Hentikan semuanya" + }, + "trigger_type": { + "press": "\"{subtype}\" ditekan", + "release": "\"{subtype}\" dilepas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ko.json b/homeassistant/components/lutron_caseta/translations/ko.json index af7fed5829c..862bfab8cfd 100644 --- a/homeassistant/components/lutron_caseta/translations/ko.json +++ b/homeassistant/components/lutron_caseta/translations/ko.json @@ -2,21 +2,75 @@ "config": { "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" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "not_lutron_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Lutron \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Lutron Cas\u00e9ta: {name} ({host})", "step": { "import_failed": { "description": "configuration.yaml \uc5d0\uc11c \uac00\uc838\uc628 \ube0c\ub9ac\uc9c0 (\ud638\uc2a4\ud2b8:{host}) \ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "title": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uad6c\uc131\uc744 \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." }, + "link": { + "description": "{name} ({host})\uacfc(\uc640) \ud398\uc5b4\ub9c1\ud558\ub824\uba74 \uc774 \uc591\uc2dd\uc744 \uc81c\ucd9c\ud55c \ud6c4 \ube0c\ub9ac\uc9c0 \ub4a4\ucabd\uc5d0 \uc788\ub294 \uac80\uc740\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", + "title": "\ube0c\ub9ac\uc9c0\uc640 \ud398\uc5b4\ub9c1\ud558\uae30" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" - } + }, + "description": "\uae30\uae30\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ube0c\ub9ac\uc9c0\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc", + "button_2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "button_3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc", + "button_4": "\ub124 \ubc88\uc9f8 \ubc84\ud2bc", + "close_1": "1 \ub2eb\uae30", + "close_2": "2 \ub2eb\uae30", + "close_3": "3 \ub2eb\uae30", + "close_4": "4 \ub2eb\uae30", + "close_all": "\ubaa8\ub450 \ub2eb\uae30", + "group_1_button_1": "\uccab \ubc88\uc9f8 \uadf8\ub8f9 \uccab \ubc88\uc9f8 \ubc84\ud2bc", + "group_1_button_2": "\uccab \ubc88\uc9f8 \uadf8\ub8f9 \ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "group_2_button_1": "\ub450 \ubc88\uc9f8 \uadf8\ub8f9 \uccab \ubc88\uc9f8 \ubc84\ud2bc", + "group_2_button_2": "\ub450 \ubc88\uc9f8 \uadf8\ub8f9 \ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "lower": "\ub0ae\ucd94\uae30", + "lower_1": "1 \ub0ae\ucd94\uae30", + "lower_2": "2 \ub0ae\ucd94\uae30", + "lower_3": "3 \ub0ae\ucd94\uae30", + "lower_4": "4 \ub0ae\ucd94\uae30", + "lower_all": "\ubaa8\ub450 \ub0ae\ucd94\uae30", + "off": "\ub044\uae30", + "on": "\ucf1c\uae30", + "open_1": "1 \uc5f4\uae30", + "open_2": "2 \uc5f4\uae30", + "open_3": "3 \uc5f4\uae30", + "open_4": "4 \uc5f4\uae30", + "open_all": "\ubaa8\ub450 \uc5f4\uae30", + "raise": "\ub192\uc774\uae30", + "raise_1": "1 \uc62c\ub9ac\uae30", + "raise_2": "2 \uc62c\ub9ac\uae30", + "raise_3": "3 \uc62c\ub9ac\uae30", + "raise_4": "4 \uc62c\ub9ac\uae30", + "raise_all": "\ubaa8\ub450 \uc62c\ub9ac\uae30", + "stop": "\uc911\uc9c0 (\uc990\uaca8 \ucc3e\uae30)", + "stop_1": "1 \uc911\uc9c0", + "stop_2": "2 \uc911\uc9c0", + "stop_3": "3 \uc911\uc9c0", + "stop_4": "4 \uc911\uc9c0", + "stop_all": "\ubaa8\ub450 \uc911\uc9c0" + }, + "trigger_type": { + "press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub838\uc744 \ub54c", + "release": "\"{subtype}\"\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/lb.json b/homeassistant/components/lutron_caseta/translations/lb.json index e78f8839797..6a390bc5a1f 100644 --- a/homeassistant/components/lutron_caseta/translations/lb.json +++ b/homeassistant/components/lutron_caseta/translations/lb.json @@ -13,5 +13,37 @@ "title": "Feeler beim import vun der Cas\u00e9ta Bridge Konfiguratioun" } } + }, + "device_automation": { + "trigger_subtype": { + "lower": "Erofsetzen", + "lower_1": "1 erofsetzen", + "lower_2": "2 erofsetzen", + "lower_3": "3 erofsetzen", + "lower_4": "4 erofsetzen", + "lower_all": "All erofsetzen", + "off": "Aus", + "on": "Un", + "open_1": "1 opmaachen", + "open_2": "2 opmaachen", + "open_3": "3 opmaachen", + "open_4": "4 opmaachen", + "open_all": "All opmaachen", + "raise": "Unhiewen", + "raise_1": "1 unhiewen", + "raise_2": "2 unhiewen", + "raise_3": "3 unhiewen", + "raise_4": "4 unhiewen", + "raise_all": "All unhiewen", + "stop_1": "Stop 1", + "stop_2": "Stop 2", + "stop_3": "Stop 3", + "stop_4": "Stop 4", + "stop_all": "All stoppen" + }, + "trigger_type": { + "press": "\"{subtype}\" gedr\u00e9ckt", + "release": "\"{subtype}\" lassgelooss" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/nl.json b/homeassistant/components/lutron_caseta/translations/nl.json index 17e6fc47fd8..b281d3cd22c 100644 --- a/homeassistant/components/lutron_caseta/translations/nl.json +++ b/homeassistant/components/lutron_caseta/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "not_lutron_device": "Ontdekt apparaat is geen Lutron-apparaat" }, "error": { "cannot_connect": "Kon niet verbinden" @@ -10,13 +11,19 @@ "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { - "description": "Kan bridge (host: {host} ) niet instellen, ge\u00efmporteerd uit configuration.yaml." + "description": "Kan bridge (host: {host} ) niet instellen, ge\u00efmporteerd uit configuration.yaml.", + "title": "Het importeren van de Cas\u00e9ta bridge configuratie is mislukt." + }, + "link": { + "description": "Om te koppelen met {name} ({host}), na het verzenden van dit formulier, druk op de zwarte knop op de achterkant van de brug.", + "title": "Koppel met de bridge" }, "user": { "data": { "host": "Host" }, - "description": "Voer het IP-adres van het apparaat in." + "description": "Voer het IP-adres van het apparaat in.", + "title": "Automatisch verbinding maken met de bridge" } } }, @@ -35,17 +42,31 @@ "group_1_button_2": "Eerste Groep tweede knop", "group_2_button_1": "Tweede Groep eerste knop", "group_2_button_2": "Tweede Groep tweede knop", + "lower": "Verlagen", + "lower_1": "Verlagen 1", + "lower_2": "Verlagen 2", + "lower_3": "Verlagen 3", + "lower_4": "Verlaag 4", + "lower_all": "Verlaag alles", "off": "Uit", "on": "Aan", "open_1": "Open 1", "open_2": "Open 2", "open_3": "Open 3", "open_4": "Open 4", + "open_all": "Open alle", + "raise": "Verhogen", + "raise_1": "Verhogen 1", + "raise_2": "Verhogen 2", + "raise_3": "Verhogen 3", + "raise_4": "Verhogen 4", + "raise_all": "Verhoog alles", "stop": "Stop (favoriet)", "stop_1": "Stop 1", "stop_2": "Stop 2", "stop_3": "Stop 3", - "stop_4": "Stop 4" + "stop_4": "Stop 4", + "stop_all": "Stop alles" }, "trigger_type": { "press": "\" {subtype} \" ingedrukt", diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 98084b28f0c..c979231a216 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -7,10 +7,9 @@ from lyft_rides.client import LyftRidesClient from lyft_rides.errors import APIError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -74,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class LyftSensor(Entity): +class LyftSensor(SensorEntity): """Implementation of an Lyft sensor.""" def __init__(self, sensorType, products, product_id, product): @@ -110,7 +109,7 @@ class LyftSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" params = { "Product ID": self._product["ride_type"], diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 12990d66ba9..c3ef18e7c7f 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -1,8 +1,10 @@ """The Honeywell Lyric integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict, Optional +from typing import Any from aiolyric import Lyric from aiolyric.objects.device import LyricDevice @@ -13,7 +15,6 @@ 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 ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -109,13 +110,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -126,8 +125,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -147,7 +146,7 @@ class LyricEntity(CoordinatorEntity): device: LyricDevice, key: str, name: str, - icon: Optional[str], + icon: str | None, ) -> None: """Initialize the Honeywell Lyric entity.""" super().__init__(coordinator) @@ -190,7 +189,7 @@ class LyricDeviceEntity(LyricEntity): """Defines a Honeywell Lyric device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this Honeywell Lyric instance.""" return { "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 41e8fa90b67..0e3672f952e 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -1,7 +1,8 @@ """Support for Honeywell Lyric climate platform.""" +from __future__ import annotations + import logging from time import gmtime, strftime, time -from typing import List, Optional from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation @@ -11,6 +12,10 @@ from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, @@ -41,6 +46,10 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +LYRIC_HVAC_ACTION_OFF = "EquipmentOff" +LYRIC_HVAC_ACTION_HEAT = "Heat" +LYRIC_HVAC_ACTION_COOL = "Cool" + LYRIC_HVAC_MODE_OFF = "Off" LYRIC_HVAC_MODE_HEAT = "Heat" LYRIC_HVAC_MODE_COOL = "Cool" @@ -60,6 +69,12 @@ HVAC_MODES = { LYRIC_HVAC_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL, } +HVAC_ACTIONS = { + LYRIC_HVAC_ACTION_OFF: CURRENT_HVAC_OFF, + LYRIC_HVAC_ACTION_HEAT: CURRENT_HVAC_HEAT, + LYRIC_HVAC_ACTION_COOL: CURRENT_HVAC_COOL, +} + SERVICE_HOLD_TIME = "set_hold_time" ATTR_TIME_PERIOD = "time_period" @@ -148,22 +163,30 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return self._temperature_unit @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self.device.indoorTemperature + @property + def hvac_action(self) -> str: + """Return the current hvac action.""" + action = HVAC_ACTIONS.get(self.device.operationStatus.mode, None) + if action == CURRENT_HVAC_OFF and self.hvac_mode != HVAC_MODE_OFF: + action = CURRENT_HVAC_IDLE + return action + @property def hvac_mode(self) -> str: """Return the hvac mode.""" return HVAC_MODES[self.device.changeableValues.mode] @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """List of available hvac modes.""" return self._hvac_modes @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" device = self.device if not device.hasDualSetpointStatus: @@ -171,7 +194,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return None @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the upper bound temperature we try to reach.""" device = self.device if device.hasDualSetpointStatus: @@ -179,7 +202,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return None @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the upper bound temperature we try to reach.""" device = self.device if device.hasDualSetpointStatus: @@ -187,12 +210,12 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return None @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return current preset mode.""" return self.device.changeableValues.thermostatSetpointStatus @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return preset modes.""" return [ PRESET_NO_HOLD, diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index 460eb6e2a3d..6aa028e2636 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", "dependencies": ["http"], - "requirements": ["aiolyric==1.0.5"], + "requirements": ["aiolyric==1.0.6"], "codeowners": ["@timmo001"], "quality_scale": "silver", "dhcp": [ diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index c4950f119d9..db90f474124 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, @@ -67,7 +68,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class LyricSensor(LyricDeviceEntity): +class LyricSensor(LyricDeviceEntity, SensorEntity): """Defines a Honeywell Lyric sensor.""" def __init__( diff --git a/homeassistant/components/lyric/services.yaml b/homeassistant/components/lyric/services.yaml index b4ea74a9644..69c802d90aa 100644 --- a/homeassistant/components/lyric/services.yaml +++ b/homeassistant/components/lyric/services.yaml @@ -1,9 +1,18 @@ set_hold_time: + name: Set Hold Time description: "Sets the time to hold until" + target: + device: + integration: lyric + entity: + integration: lyric + domain: climate fields: - entity_id: - description: Name(s) of entities to change - example: "climate.thermostat" time_period: + name: Time Period description: Time to hold until + default: "01:00:00" example: "01:00:00" + required: true + selector: + text: diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json new file mode 100644 index 00000000000..cae1f6d20c0 --- /dev/null +++ b/homeassistant/components/lyric/translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json new file mode 100644 index 00000000000..876fe2f8c39 --- /dev/null +++ b/homeassistant/components/lyric/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 9364bee27b2..0dd27a60ae0 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -5,7 +5,7 @@ import logging import magicseaweed import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_NAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -90,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class MagicSeaweedSensor(Entity): +class MagicSeaweedSensor(SensorEntity): """Implementation of a MagicSeaweed sensor.""" def __init__(self, forecast_data, sensor_type, name, unit_system, hour=None): @@ -136,7 +135,7 @@ class MagicSeaweedSensor(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 220b6a1abc1..39ee6e46350 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -51,11 +51,14 @@ async def handle_webhook(hass, webhook_id, request): except ValueError: return None - if isinstance(data, dict) and "signature" in data: - if await verify_webhook(hass, **data["signature"]): - data["webhook_id"] = webhook_id - hass.bus.async_fire(MESSAGE_RECEIVED, data) - return + if ( + isinstance(data, dict) + and "signature" in data + and await verify_webhook(hass, **data["signature"]) + ): + data["webhook_id"] = webhook_id + hass.bus.async_fire(MESSAGE_RECEIVED, data) + return _LOGGER.warning( "Mailgun webhook received an unauthenticated message - webhook_id: %s", diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index 51bbe6ef04c..14c2293734c 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks Mailgun-al] ( {mailgun_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/id.json b/homeassistant/components/mailgun/translations/id.json new file mode 100644 index 00000000000..b58deb171be --- /dev/null +++ b/homeassistant/components/mailgun/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [Webhooks dengan Mailgun]({mailgun_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan Mailgun?", + "title": "Siapkan Mailgun Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ko.json b/homeassistant/components/mailgun/translations/ko.json index b757a27f4a0..2a296303d58 100644 --- a/homeassistant/components/mailgun/translations/ko.json +++ b/homeassistant/components/mailgun/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun \uc6f9 \ud6c5]({mailgun_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Mailgun \uc6f9 \ud6c5]({mailgun_url})\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \nHome Assistant\ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 2313bcace19..00c155615ee 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -394,7 +394,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): return check @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.state == STATE_ALARM_PENDING or self.state == STATE_ALARM_ARMING: return { diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index f11938396a7..2fa0e631c1d 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -415,7 +415,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): return check @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self.state != STATE_ALARM_PENDING: return {} diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index c89de5552d5..62af53079e8 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,12 +1,13 @@ """The Matrix bot component.""" from functools import partial import logging +import mimetypes import os from matrix_client.client import MatrixClient, MatrixRequestError import voluptuous as vol -from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET +from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -31,8 +32,12 @@ CONF_COMMANDS = "commands" CONF_WORD = "word" CONF_EXPRESSION = "expression" +DEFAULT_CONTENT_TYPE = "application/octet-stream" + EVENT_MATRIX_COMMAND = "matrix_command" +ATTR_IMAGES = "images" # optional images + COMMAND_SCHEMA = vol.All( vol.Schema( { @@ -67,6 +72,9 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_DATA): { + vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]), + }, vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), } ) @@ -336,13 +344,20 @@ class MatrixBot: return _client - def _send_message(self, message, target_rooms): - """Send the message to the Matrix server.""" + def _send_image(self, img, target_rooms): + _LOGGER.debug("Uploading file from path, %s", img) + if not self.hass.config.is_allowed_path(img): + _LOGGER.error("Path not allowed: %s", img) + return + with open(img, "rb") as upfile: + imgfile = upfile.read() + content_type = mimetypes.guess_type(img)[0] + mxc = self._client.upload(imgfile, content_type) for target_room in target_rooms: try: room = self._join_or_get_room(target_room) - _LOGGER.debug(room.send_text(message)) + room.send_image(mxc, img) except MatrixRequestError as ex: _LOGGER.error( "Unable to deliver message to room '%s': %d, %s", @@ -351,6 +366,28 @@ class MatrixBot: ex.content, ) + def _send_message(self, message, data, target_rooms): + """Send the message to the Matrix server.""" + for target_room in target_rooms: + try: + room = self._join_or_get_room(target_room) + if message is not None: + _LOGGER.debug(room.send_text(message)) + except MatrixRequestError as ex: + _LOGGER.error( + "Unable to deliver message to room '%s': %d, %s", + target_room, + ex.code, + ex.content, + ) + if data is not None: + for img in data.get(ATTR_IMAGES, []): + self._send_image(img, target_rooms) + def handle_send_message(self, service): """Handle the send_message service.""" - self._send_message(service.data[ATTR_MESSAGE], service.data[ATTR_TARGET]) + self._send_message( + service.data.get(ATTR_MESSAGE), + service.data.get(ATTR_DATA), + service.data[ATTR_TARGET], + ) diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 0965783bf4d..8643d7511bc 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -2,6 +2,7 @@ import voluptuous as vol from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, @@ -31,9 +32,10 @@ class MatrixNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send the message to the Matrix server.""" target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] - service_data = {ATTR_TARGET: target_rooms, ATTR_MESSAGE: message} - + data = kwargs.get(ATTR_DATA) + if data is not None: + service_data[ATTR_DATA] = data return self.hass.services.call( DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data ) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index f8b0c53bda6..fe99bf6365a 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -7,3 +7,6 @@ send_message: target: description: A list of room(s) to send the message to. example: "#hasstest:matrix.org" + data: + description: Extended information of notification. Supports list of images. Optional. + example: "{'images': ['/tmp/test.jpg']}" diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index ffd156b5e00..e38f08809a7 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -4,7 +4,6 @@ from socket import timeout from threading import Lock import time -from maxcube.connection import MaxCubeConnection from maxcube.cube import MaxCube import voluptuous as vol @@ -60,7 +59,7 @@ def setup(hass, config): scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() try: - cube = MaxCube(MaxCubeConnection(host, port)) + cube = MaxCube(host, port) hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) except timeout as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) @@ -86,6 +85,7 @@ class MaxCubeHandle: def __init__(self, cube, scan_interval): """Initialize the Cube Handle.""" self.cube = cube + self.cube.use_persistent_connection = scan_interval <= 300 # seconds self.scan_interval = scan_interval self.mutex = Lock() self._updatets = time.monotonic() diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 376076352a6..223c0e3fc99 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -11,13 +11,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Iterate through all MAX! Devices and add window shutters.""" devices = [] for handler in hass.data[DATA_KEY].values(): - cube = handler.cube - for device in cube.devices: - name = f"{cube.room_by_id(device.room_id).name} {device.name}" - + for device in handler.cube.devices: # Only add Window Shutters if device.is_windowshutter(): - devices.append(MaxCubeShutter(handler, name, device.rf_address)) + devices.append(MaxCubeShutter(handler, device)) if devices: add_entities(devices) @@ -26,13 +23,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MaxCubeShutter(BinarySensorEntity): """Representation of a MAX! Cube Binary Sensor device.""" - def __init__(self, handler, name, rf_address): + def __init__(self, handler, device): """Initialize MAX! Cube BinarySensorEntity.""" - self._name = name - self._sensor_type = DEVICE_CLASS_WINDOW - self._rf_address = rf_address + room = handler.cube.room_by_id(device.room_id) + self._name = f"{room.name} {device.name}" self._cubehandle = handler - self._state = None + self._device = device @property def name(self): @@ -42,15 +38,13 @@ class MaxCubeShutter(BinarySensorEntity): @property def device_class(self): """Return the class of this sensor.""" - return self._sensor_type + return DEVICE_CLASS_WINDOW @property def is_on(self): """Return true if the binary sensor is on/open.""" - return self._state + return self._device.is_open def update(self): """Get latest data from MAX! Cube.""" self._cubehandle.update() - device = self._cubehandle.cube.device_by_rf(self._rf_address) - self._state = device.is_open diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index c17cc988c1d..75ee7ef21f0 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -1,4 +1,6 @@ """Support for MAX! Thermostats via MAX! Cube.""" +from __future__ import annotations + import logging import socket @@ -47,31 +49,14 @@ MAX_TEMPERATURE = 30.0 SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -HASS_PRESET_TO_MAX_MODE = { - PRESET_AWAY: MAX_DEVICE_MODE_VACATION, - PRESET_BOOST: MAX_DEVICE_MODE_BOOST, - PRESET_NONE: MAX_DEVICE_MODE_AUTOMATIC, - PRESET_ON: MAX_DEVICE_MODE_MANUAL, -} - -MAX_MODE_TO_HASS_PRESET = { - MAX_DEVICE_MODE_AUTOMATIC: PRESET_NONE, - MAX_DEVICE_MODE_BOOST: PRESET_BOOST, - MAX_DEVICE_MODE_MANUAL: PRESET_NONE, - MAX_DEVICE_MODE_VACATION: PRESET_AWAY, -} - def setup_platform(hass, config, add_entities, discovery_info=None): """Iterate through all MAX! Devices and add thermostats.""" devices = [] for handler in hass.data[DATA_KEY].values(): - cube = handler.cube - for device in cube.devices: - name = f"{cube.room_by_id(device.room_id).name} {device.name}" - + for device in handler.cube.devices: if device.is_thermostat() or device.is_wallthermostat(): - devices.append(MaxCubeClimate(handler, name, device.rf_address)) + devices.append(MaxCubeClimate(handler, device)) if devices: add_entities(devices) @@ -80,11 +65,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MaxCubeClimate(ClimateEntity): """MAX! Cube ClimateEntity.""" - def __init__(self, handler, name, rf_address): + def __init__(self, handler, device): """Initialize MAX! Cube ClimateEntity.""" - self._name = name - self._rf_address = rf_address + room = handler.cube.room_by_id(device.room_id) + self._name = f"{room.name} {device.name}" self._cubehandle = handler + self._device = device @property def supported_features(self): @@ -104,18 +90,15 @@ class MaxCubeClimate(ClimateEntity): @property def min_temp(self): """Return the minimum temperature.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - if device.min_temperature is None: - return MIN_TEMPERATURE - return device.min_temperature + temp = self._device.min_temperature or MIN_TEMPERATURE + # OFF_TEMPERATURE (always off) a is valid temperature to maxcube but not to Home Assistant. + # We use HVAC_MODE_OFF instead to represent a turned off thermostat. + return max(temp, MIN_TEMPERATURE) @property def max_temp(self): """Return the maximum temperature.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - if device.max_temperature is None: - return MAX_TEMPERATURE - return device.max_temperature + return self._device.max_temperature or MAX_TEMPERATURE @property def temperature_unit(self): @@ -125,18 +108,17 @@ class MaxCubeClimate(ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - return device.actual_temperature + return self._device.actual_temperature @property def hvac_mode(self): """Return current operation mode.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - if device.mode in [MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_BOOST]: + mode = self._device.mode + if mode in [MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_BOOST]: return HVAC_MODE_AUTO if ( - device.mode == MAX_DEVICE_MODE_MANUAL - and device.target_temperature == OFF_TEMPERATURE + mode == MAX_DEVICE_MODE_MANUAL + and self._device.target_temperature == OFF_TEMPERATURE ): return HVAC_MODE_OFF @@ -149,41 +131,46 @@ class MaxCubeClimate(ClimateEntity): def set_hvac_mode(self, hvac_mode: str): """Set new target hvac mode.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - temp = device.target_temperature - mode = MAX_DEVICE_MODE_MANUAL - if hvac_mode == HVAC_MODE_OFF: - temp = OFF_TEMPERATURE - elif hvac_mode != HVAC_MODE_HEAT: - # Reset the temperature to a sane value. - # Ideally, we should send 0 and the device will set its - # temperature according to the schedule. However, current - # version of the library has a bug which causes an - # exception when setting values below 8. - if temp in [OFF_TEMPERATURE, ON_TEMPERATURE]: - temp = device.eco_temperature - mode = MAX_DEVICE_MODE_AUTOMATIC + self._set_target(MAX_DEVICE_MODE_MANUAL, OFF_TEMPERATURE) + elif hvac_mode == HVAC_MODE_HEAT: + temp = max(self._device.target_temperature, self.min_temp) + self._set_target(MAX_DEVICE_MODE_MANUAL, temp) + elif hvac_mode == HVAC_MODE_AUTO: + self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None) + else: + raise ValueError(f"unsupported HVAC mode {hvac_mode}") - cube = self._cubehandle.cube + def _set_target(self, mode: int | None, temp: float | None) -> None: + """ + Set the mode and/or temperature of the thermostat. + + @param mode: this is the mode to change to. + @param temp: the temperature to target. + + Both parameters are optional. When mode is undefined, it keeps + the previous mode. When temp is undefined, it fetches the + temperature from the weekly schedule when mode is + MAX_DEVICE_MODE_AUTOMATIC and keeps the previous + temperature otherwise. + """ with self._cubehandle.mutex: try: - cube.set_temperature_mode(device, temp, mode) + self._cubehandle.cube.set_temperature_mode(self._device, temp, mode) except (socket.timeout, OSError): _LOGGER.error("Setting HVAC mode failed") - return @property def hvac_action(self): """Return the current running hvac operation if supported.""" - cube = self._cubehandle.cube - device = cube.device_by_rf(self._rf_address) valve = 0 - if device.is_thermostat(): - valve = device.valve_position - elif device.is_wallthermostat(): - for device in cube.devices_by_room(cube.room_by_id(device.room_id)): + if self._device.is_thermostat(): + valve = self._device.valve_position + elif self._device.is_wallthermostat(): + cube = self._cubehandle.cube + room = cube.room_by_id(self._device.room_id) + for device in cube.devices_by_room(room): if device.is_thermostat() and device.valve_position > 0: valve = device.valve_position break @@ -201,49 +188,35 @@ class MaxCubeClimate(ClimateEntity): @property def target_temperature(self): """Return the temperature we try to reach.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - if ( - device.target_temperature is None - or device.target_temperature < self.min_temp - or device.target_temperature > self.max_temp - ): + temp = self._device.target_temperature + if temp is None or temp < self.min_temp or temp > self.max_temp: return None - return device.target_temperature + return temp def set_temperature(self, **kwargs): """Set new target temperatures.""" - if kwargs.get(ATTR_TEMPERATURE) is None: - return False - - target_temperature = kwargs.get(ATTR_TEMPERATURE) - device = self._cubehandle.cube.device_by_rf(self._rf_address) - - cube = self._cubehandle.cube - - with self._cubehandle.mutex: - try: - cube.set_target_temperature(device, target_temperature) - except (socket.timeout, OSError): - _LOGGER.error("Setting target temperature failed") - return False + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is None: + raise ValueError( + f"No {ATTR_TEMPERATURE} parameter passed to set_temperature method." + ) + self._set_target(None, temp) @property def preset_mode(self): """Return the current preset mode.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - if self.hvac_mode == HVAC_MODE_OFF: - return PRESET_NONE - - if device.mode == MAX_DEVICE_MODE_MANUAL: - if device.target_temperature == device.comfort_temperature: + if self._device.mode == MAX_DEVICE_MODE_MANUAL: + if self._device.target_temperature == self._device.comfort_temperature: return PRESET_COMFORT - if device.target_temperature == device.eco_temperature: + if self._device.target_temperature == self._device.eco_temperature: return PRESET_ECO - if device.target_temperature == ON_TEMPERATURE: + if self._device.target_temperature == ON_TEMPERATURE: return PRESET_ON - return PRESET_NONE - - return MAX_MODE_TO_HASS_PRESET[device.mode] + elif self._device.mode == MAX_DEVICE_MODE_BOOST: + return PRESET_BOOST + elif self._device.mode == MAX_DEVICE_MODE_VACATION: + return PRESET_AWAY + return PRESET_NONE @property def preset_modes(self): @@ -259,37 +232,27 @@ class MaxCubeClimate(ClimateEntity): def set_preset_mode(self, preset_mode): """Set new operation mode.""" - device = self._cubehandle.cube.device_by_rf(self._rf_address) - temp = device.target_temperature - mode = MAX_DEVICE_MODE_AUTOMATIC - - if preset_mode in [PRESET_COMFORT, PRESET_ECO, PRESET_ON]: - mode = MAX_DEVICE_MODE_MANUAL - if preset_mode == PRESET_COMFORT: - temp = device.comfort_temperature - elif preset_mode == PRESET_ECO: - temp = device.eco_temperature - else: - temp = ON_TEMPERATURE + if preset_mode == PRESET_COMFORT: + self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.comfort_temperature) + elif preset_mode == PRESET_ECO: + self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.eco_temperature) + elif preset_mode == PRESET_ON: + self._set_target(MAX_DEVICE_MODE_MANUAL, ON_TEMPERATURE) + elif preset_mode == PRESET_AWAY: + self._set_target(MAX_DEVICE_MODE_VACATION, None) + elif preset_mode == PRESET_BOOST: + self._set_target(MAX_DEVICE_MODE_BOOST, None) + elif preset_mode == PRESET_NONE: + self._set_target(MAX_DEVICE_MODE_AUTOMATIC, None) else: - mode = HASS_PRESET_TO_MAX_MODE[preset_mode] or MAX_DEVICE_MODE_AUTOMATIC - - with self._cubehandle.mutex: - try: - self._cubehandle.cube.set_temperature_mode(device, temp, mode) - except (socket.timeout, OSError): - _LOGGER.error("Setting operation mode failed") - return + raise ValueError(f"unsupported preset mode {preset_mode}") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" - cube = self._cubehandle.cube - device = cube.device_by_rf(self._rf_address) - - if not device.is_thermostat(): + if not self._device.is_thermostat(): return {} - return {ATTR_VALVE_POSITION: device.valve_position} + return {ATTR_VALVE_POSITION: self._device.valve_position} def update(self): """Get latest data from MAX! Cube.""" diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index e6badb254f7..ddc21bd2358 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -2,6 +2,6 @@ "domain": "maxcube", "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": ["maxcube-api==0.3.0"], + "requirements": ["maxcube-api==0.4.1"], "codeowners": [] } diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 14b33df66c0..2f4e0e84f13 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -32,6 +32,12 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] +async def with_timeout(task, timeout_seconds=10): + """Run an async task with a timeout.""" + async with async_timeout.timeout(timeout_seconds): + return await task + + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Mazda Connected Services component.""" hass.data[DOMAIN] = {} @@ -69,11 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data(): """Fetch data from Mazda API.""" - - async def with_timeout(task): - async with async_timeout.timeout(10): - return await task - try: vehicles = await with_timeout(mazda_client.get_vehicles()) @@ -116,14 +117,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): } # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() # Setup components - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -134,8 +133,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py index 53c08b9bd69..3c1137b8e80 100644 --- a/homeassistant/components/mazda/config_flow.py +++ b/homeassistant/components/mazda/config_flow.py @@ -13,9 +13,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.helpers import aiohttp_client -# https://github.com/PyCQA/pylint/issues/3202 -from .const import DOMAIN # pylint: disable=unused-import -from .const import MAZDA_REGIONS +from .const import DOMAIN, MAZDA_REGIONS _LOGGER = logging.getLogger(__name__) @@ -40,15 +38,15 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: await self.async_set_unique_id(user_input[CONF_EMAIL].lower()) + websession = aiohttp_client.async_get_clientsession(self.hass) + mazda_client = MazdaAPI( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + user_input[CONF_REGION], + websession, + ) 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" diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index b3826d42318..c3a05a351c3 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.0.8"], + "requirements": ["pymazda==0.0.9"], "codeowners": ["@bdr99"], "quality_scale": "platinum" } \ No newline at end of file diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index fa03eb7f410..7382347e6de 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -1,4 +1,5 @@ """Platform for Mazda sensor integration.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -29,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class MazdaFuelRemainingSensor(MazdaEntity): +class MazdaFuelRemainingSensor(MazdaEntity, SensorEntity): """Class for the fuel remaining sensor.""" @property @@ -59,7 +60,7 @@ class MazdaFuelRemainingSensor(MazdaEntity): return self.coordinator.data[self.index]["status"]["fuelRemainingPercent"] -class MazdaFuelDistanceSensor(MazdaEntity): +class MazdaFuelDistanceSensor(MazdaEntity, SensorEntity): """Class for the fuel distance sensor.""" @property @@ -91,10 +92,16 @@ class MazdaFuelDistanceSensor(MazdaEntity): fuel_distance_km = self.coordinator.data[self.index]["status"][ "fuelDistanceRemainingKm" ] - return round(self.hass.config.units.length(fuel_distance_km, LENGTH_KILOMETERS)) + return ( + None + if fuel_distance_km is None + else round( + self.hass.config.units.length(fuel_distance_km, LENGTH_KILOMETERS) + ) + ) -class MazdaOdometerSensor(MazdaEntity): +class MazdaOdometerSensor(MazdaEntity, SensorEntity): """Class for the odometer sensor.""" @property @@ -124,10 +131,14 @@ class MazdaOdometerSensor(MazdaEntity): def state(self): """Return the state of the sensor.""" odometer_km = self.coordinator.data[self.index]["status"]["odometerKm"] - return round(self.hass.config.units.length(odometer_km, LENGTH_KILOMETERS)) + return ( + None + if odometer_km is None + else round(self.hass.config.units.length(odometer_km, LENGTH_KILOMETERS)) + ) -class MazdaFrontLeftTirePressureSensor(MazdaEntity): +class MazdaFrontLeftTirePressureSensor(MazdaEntity, SensorEntity): """Class for the front left tire pressure sensor.""" @property @@ -154,14 +165,13 @@ class MazdaFrontLeftTirePressureSensor(MazdaEntity): @property def state(self): """Return the state of the sensor.""" - return round( - self.coordinator.data[self.index]["status"]["tirePressure"][ - "frontLeftTirePressurePsi" - ] - ) + tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ + "frontLeftTirePressurePsi" + ] + return None if tire_pressure is None else round(tire_pressure) -class MazdaFrontRightTirePressureSensor(MazdaEntity): +class MazdaFrontRightTirePressureSensor(MazdaEntity, SensorEntity): """Class for the front right tire pressure sensor.""" @property @@ -188,14 +198,13 @@ class MazdaFrontRightTirePressureSensor(MazdaEntity): @property def state(self): """Return the state of the sensor.""" - return round( - self.coordinator.data[self.index]["status"]["tirePressure"][ - "frontRightTirePressurePsi" - ] - ) + tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ + "frontRightTirePressurePsi" + ] + return None if tire_pressure is None else round(tire_pressure) -class MazdaRearLeftTirePressureSensor(MazdaEntity): +class MazdaRearLeftTirePressureSensor(MazdaEntity, SensorEntity): """Class for the rear left tire pressure sensor.""" @property @@ -222,14 +231,13 @@ class MazdaRearLeftTirePressureSensor(MazdaEntity): @property def state(self): """Return the state of the sensor.""" - return round( - self.coordinator.data[self.index]["status"]["tirePressure"][ - "rearLeftTirePressurePsi" - ] - ) + tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ + "rearLeftTirePressurePsi" + ] + return None if tire_pressure is None else round(tire_pressure) -class MazdaRearRightTirePressureSensor(MazdaEntity): +class MazdaRearRightTirePressureSensor(MazdaEntity, SensorEntity): """Class for the rear right tire pressure sensor.""" @property @@ -256,8 +264,7 @@ class MazdaRearRightTirePressureSensor(MazdaEntity): @property def state(self): """Return the state of the sensor.""" - return round( - self.coordinator.data[self.index]["status"]["tirePressure"][ - "rearRightTirePressurePsi" - ] - ) + tire_pressure = self.coordinator.data[self.index]["status"]["tirePressure"][ + "rearRightTirePressurePsi" + ] + return None if tire_pressure is None else round(tire_pressure) diff --git a/homeassistant/components/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json new file mode 100644 index 00000000000..6f3c5a54f3f --- /dev/null +++ b/homeassistant/components/mazda/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/de.json b/homeassistant/components/mazda/translations/de.json index 4e23becb8af..9050ee9f00c 100644 --- a/homeassistant/components/mazda/translations/de.json +++ b/homeassistant/components/mazda/translations/de.json @@ -5,6 +5,7 @@ "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { + "account_locked": "Konto gesperrt. Bitte versuchen Sie es sp\u00e4ter erneut.", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" @@ -15,15 +16,20 @@ "email": "E-Mail", "password": "Passwort", "region": "Region" - } + }, + "description": "Die Authentifizierung f\u00fcr Mazda Connected Services ist fehlgeschlagen. Bitte geben Sie Ihre aktuellen Anmeldedaten ein.", + "title": "Mazda Connected Services - Authentifizierung fehlgeschlagen" }, "user": { "data": { "email": "E-Mail", "password": "Passwort", "region": "Region" - } + }, + "description": "Bitte geben Sie die E-Mail-Adresse und das Passwort ein, die Sie f\u00fcr die Anmeldung bei der MyMazda Mobile App verwenden.", + "title": "Mazda Connected Services - Konto hinzuf\u00fcgen" } } - } + }, + "title": "Mazda Connected Services" } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json new file mode 100644 index 00000000000..1b9c6893ed5 --- /dev/null +++ b/homeassistant/components/mazda/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "account_locked": "Fi\u00f3k z\u00e1rolva. K\u00e9rlek, pr\u00f3b\u00e1ld \u00fajra k\u00e9s\u0151bb.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3", + "region": "R\u00e9gi\u00f3" + }, + "title": "Mazda Connected Services - A hiteles\u00edt\u00e9s sikertelen" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3", + "region": "R\u00e9gi\u00f3" + }, + "title": "Mazda Connected Services - Fi\u00f3k hozz\u00e1ad\u00e1sa" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/id.json b/homeassistant/components/mazda/translations/id.json new file mode 100644 index 00000000000..0a6e81e8454 --- /dev/null +++ b/homeassistant/components/mazda/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "account_locked": "Akun terkunci. Coba lagi nanti.", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth": { + "data": { + "email": "Email", + "password": "Kata Sandi", + "region": "Wilayah" + }, + "description": "Autentikasi gagal untuk Mazda Connected Services. Masukkan kredensial Anda saat ini.", + "title": "Mazda Connected Services - Autentikasi Gagal" + }, + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi", + "region": "Wilayah" + }, + "description": "Masukkan alamat email dan kata sandi yang digunakan untuk masuk ke aplikasi seluler MyMazda.", + "title": "Mazda Connected Services - Tambahkan Akun" + } + } + }, + "title": "Mazda Connected Services" +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/ko.json b/homeassistant/components/mazda/translations/ko.json index 31495b0d8e3..aa9dcf99d14 100644 --- a/homeassistant/components/mazda/translations/ko.json +++ b/homeassistant/components/mazda/translations/ko.json @@ -5,6 +5,7 @@ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { + "account_locked": "\uacc4\uc815\uc774 \uc7a0\uacbc\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.", "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" @@ -13,15 +14,22 @@ "reauth": { "data": { "email": "\uc774\uba54\uc77c", - "password": "\ube44\ubc00\ubc88\ud638" - } + "password": "\ube44\ubc00\ubc88\ud638", + "region": "\uc9c0\uc5ed" + }, + "description": "Mazda Connected Services\uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Mazda Connected Services - \uc778\uc99d \uc2e4\ud328" }, "user": { "data": { "email": "\uc774\uba54\uc77c", - "password": "\ube44\ubc00\ubc88\ud638" - } + "password": "\ube44\ubc00\ubc88\ud638", + "region": "\uc9c0\uc5ed" + }, + "description": "MyMazda \ubaa8\ubc14\uc77c \uc571\uc5d0 \ub85c\uadf8\uc778\ud558\uae30 \uc704\ud574 \uc0ac\uc6a9\ud558\ub294 \uc774\uba54\uc77c \uc8fc\uc18c\uc640 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Mazda Connected Services - \uacc4\uc815 \ucd94\uac00\ud558\uae30" } } - } + }, + "title": "Mazda Connected Services" } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json index 3198bfb4192..64975c9b14b 100644 --- a/homeassistant/components/mazda/translations/nl.json +++ b/homeassistant/components/mazda/translations/nl.json @@ -14,15 +14,20 @@ "reauth": { "data": { "email": "E-mail", - "password": "Wachtwoord" - } + "password": "Wachtwoord", + "region": "Regio" + }, + "description": "Verificatie mislukt voor Mazda Connected Services. Voer uw huidige inloggegevens in.", + "title": "Mazda Connected Services - Authenticatie mislukt" }, "user": { "data": { "email": "E-mail", "password": "Wachtwoord", "region": "Regio" - } + }, + "description": "Voer het e-mailadres en wachtwoord in dat u gebruikt om in te loggen op de MyMazda mobiele app.", + "title": "Mazda Connected Services - Account toevoegen" } } }, diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index f6dafad43ac..c650393a26f 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -1,8 +1,8 @@ """Support for binary sensor using I2C MCP23017 chip.""" -from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error -import board # pylint: disable=import-error -import busio # pylint: disable=import-error -import digitalio # pylint: disable=import-error +from adafruit_mcp230xx.mcp23017 import MCP23017 +import board +import busio +import digitalio import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index aeda638710e..7460529f8fe 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -3,7 +3,7 @@ "name": "MCP23017 I/O Expander", "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ - "RPi.GPIO==0.7.0", + "RPi.GPIO==0.7.1a4", "adafruit-circuitpython-mcp230xx==2.2.2" ], "codeowners": ["@jardiamj"] diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index d22593a4c3e..6b1ced540ae 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -1,8 +1,8 @@ """Support for switch sensor using I2C MCP23017 chip.""" -from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error -import board # pylint: disable=import-error -import busio # pylint: disable=import-error -import digitalio # pylint: disable=import-error +from adafruit_mcp230xx.mcp23017 import MCP23017 +import board +import busio +import digitalio import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index c6ee6ccb8a4..35a5b098184 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2021.01.24.1"], + "requirements": ["youtube_dl==2021.03.14"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 87ecff7a54c..f98c6eeceaf 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -4,12 +4,13 @@ from __future__ import annotations import asyncio import base64 import collections +from contextlib import suppress from datetime import timedelta import functools as ft import hashlib import logging import secrets -from typing import List, Optional, Tuple +from typing import final from urllib.parse import urlparse from aiohttp import web @@ -65,6 +66,7 @@ from homeassistant.loader import bind_hass from .const import ( ATTR_APP_ID, ATTR_APP_NAME, + ATTR_GROUP_MEMBERS, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ALBUM_ARTIST, @@ -95,11 +97,14 @@ from .const import ( MEDIA_CLASS_DIRECTORY, REPEAT_MODES, SERVICE_CLEAR_PLAYLIST, + SERVICE_JOIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, + SERVICE_UNJOIN, SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -300,6 +305,12 @@ async def async_setup(hass, config): "async_media_seek", [SUPPORT_SEEK], ) + component.async_register_entity_service( + SERVICE_JOIN, + {vol.Required(ATTR_GROUP_MEMBERS): list}, + "async_join_players", + [SUPPORT_GROUPING], + ) component.async_register_entity_service( SERVICE_SELECT_SOURCE, {vol.Required(ATTR_INPUT_SOURCE): cv.string}, @@ -331,6 +342,9 @@ async def async_setup(hass, config): "async_set_shuffle", [SUPPORT_SHUFFLE_SET], ) + component.async_register_entity_service( + SERVICE_UNJOIN, {}, "async_unjoin_player", [SUPPORT_GROUPING] + ) component.async_register_entity_service( SERVICE_REPEAT_SET, @@ -355,7 +369,7 @@ async def async_unload_entry(hass, entry): class MediaPlayerEntity(Entity): """ABC for media player entities.""" - _access_token: Optional[str] = None + _access_token: str | None = None # Implement these for your media player @property @@ -439,8 +453,8 @@ class MediaPlayerEntity(Entity): self, media_content_type: str, media_content_id: str, - media_image_id: Optional[str] = None, - ) -> Tuple[Optional[str], Optional[str]]: + media_image_id: str | None = None, + ) -> tuple[str | None, str | None]: """ Optionally fetch internally accessible image for media browser. @@ -538,6 +552,11 @@ class MediaPlayerEntity(Entity): """Return current repeat mode.""" return None + @property + def group_members(self): + """List of members which are currently grouped together.""" + return None + @property def supported_features(self): """Flag media player features that are supported.""" @@ -739,6 +758,11 @@ class MediaPlayerEntity(Entity): """Boolean if shuffle is supported.""" return bool(self.supported_features & SUPPORT_SHUFFLE_SET) + @property + def support_grouping(self): + """Boolean if player grouping is supported.""" + return bool(self.supported_features & SUPPORT_GROUPING) + async def async_toggle(self): """Toggle the power on the media player.""" if hasattr(self, "toggle"): @@ -831,6 +855,7 @@ class MediaPlayerEntity(Entity): return data + @final @property def state_attributes(self): """Return the state attributes.""" @@ -847,12 +872,15 @@ class MediaPlayerEntity(Entity): if self.media_image_remotely_accessible: state_attr["entity_picture_local"] = self.media_image_local + if self.support_grouping: + state_attr[ATTR_GROUP_MEMBERS] = self.group_members + return state_attr async def async_browse_media( self, - media_content_type: Optional[str] = None, - media_content_id: Optional[str] = None, + media_content_type: str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Return a BrowseMedia instance. @@ -861,6 +889,22 @@ class MediaPlayerEntity(Entity): """ raise NotImplementedError() + def join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" + raise NotImplementedError() + + async def async_join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" + await self.hass.async_add_executor_job(self.join_players, group_members) + + def unjoin_player(self): + """Remove this player from any group.""" + raise NotImplementedError() + + async def async_unjoin_player(self): + """Remove this player from any group.""" + await self.hass.async_add_executor_job(self.unjoin_player) + async def _async_fetch_image_from_cache(self, url): """Fetch image. @@ -892,18 +936,13 @@ class MediaPlayerEntity(Entity): """Retrieve an image.""" content, content_type = (None, None) websession = async_get_clientsession(self.hass) - try: - with async_timeout.timeout(10): - response = await websession.get(url) - - if response.status == HTTP_OK: - content = await response.read() - content_type = response.headers.get(CONTENT_TYPE) - if content_type: - content_type = content_type.split(";")[0] - - except asyncio.TimeoutError: - pass + with suppress(asyncio.TimeoutError), async_timeout.timeout(10): + response = await websession.get(url) + if response.status == HTTP_OK: + content = await response.read() + content_type = response.headers.get(CONTENT_TYPE) + if content_type: + content_type = content_type.split(";")[0] if content is None: _LOGGER.warning("Error retrieving proxied image from %s", url) @@ -914,7 +953,7 @@ class MediaPlayerEntity(Entity): self, media_content_type: str, media_content_id: str, - media_image_id: Optional[str] = None, + media_image_id: str | None = None, ) -> str: """Generate an url for a media browser image.""" url_path = ( @@ -947,8 +986,8 @@ class MediaPlayerImageView(HomeAssistantView): self, request: web.Request, entity_id: str, - media_content_type: Optional[str] = None, - media_content_id: Optional[str] = None, + media_content_type: str | None = None, + media_content_id: str | None = None, ) -> web.Response: """Start a get request.""" player = self.component.get_entity(entity_id) @@ -1047,7 +1086,7 @@ async def websocket_browse_media(hass, connection, msg): To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() """ component = hass.data[DOMAIN] - player: Optional[MediaPlayerDevice] = component.get_entity(msg["entity_id"]) + player: MediaPlayerDevice | None = component.get_entity(msg["entity_id"]) if player is None: connection.send_error(msg["id"], "entity_not_found", "Entity not found") @@ -1119,9 +1158,9 @@ class BrowseMedia: title: str, can_play: bool, can_expand: bool, - children: Optional[List["BrowseMedia"]] = None, - children_media_class: Optional[str] = None, - thumbnail: Optional[str] = None, + children: list[BrowseMedia] | None = None, + children_media_class: str | None = None, + thumbnail: str | None = None, ): """Initialize browse media item.""" self.media_class = media_class diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 87ccca75d36..67f4331aa60 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -2,6 +2,7 @@ ATTR_APP_ID = "app_id" ATTR_APP_NAME = "app_name" +ATTR_GROUP_MEMBERS = "group_members" ATTR_INPUT_SOURCE = "source" ATTR_INPUT_SOURCE_LIST = "source_list" ATTR_MEDIA_ALBUM_ARTIST = "media_album_artist" @@ -75,9 +76,11 @@ MEDIA_TYPE_URL = "url" MEDIA_TYPE_VIDEO = "video" SERVICE_CLEAR_PLAYLIST = "clear_playlist" +SERVICE_JOIN = "join" SERVICE_PLAY_MEDIA = "play_media" SERVICE_SELECT_SOUND_MODE = "select_sound_mode" SERVICE_SELECT_SOURCE = "select_source" +SERVICE_UNJOIN = "unjoin" REPEAT_MODE_ALL = "all" REPEAT_MODE_OFF = "off" @@ -103,3 +106,4 @@ SUPPORT_SHUFFLE_SET = 32768 SUPPORT_SELECT_SOUND_MODE = 65536 SUPPORT_BROWSE_MEDIA = 131072 SUPPORT_REPEAT_SET = 262144 +SUPPORT_GROUPING = 524288 diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 6faa6521b70..0e6e0f96c40 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -1,5 +1,5 @@ """Provides device automations for Media player.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -35,7 +35,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Media player devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index 6db5f16cf01..889bc776962 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Media player.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, + CONF_FOR, CONF_PLATFORM, CONF_TYPE, STATE_IDLE, @@ -30,11 +31,12 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Media player entities.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -59,6 +61,15 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return triggers +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -66,8 +77,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] == "turned_on": to_state = STATE_ON elif config[CONF_TYPE] == "turned_off": @@ -84,6 +93,8 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_trigger.CONF_TO: to_state, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] state_config = state_trigger.TRIGGER_SCHEMA(state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 64955d1913b..1707109197f 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -1,6 +1,8 @@ """Module that groups code required to handle state restore for component.""" +from __future__ import annotations + import asyncio -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( SERVICE_MEDIA_PAUSE, @@ -40,8 +42,8 @@ async def _async_reproduce_states( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" @@ -104,8 +106,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" await asyncio.gather( diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index eaca8483be1..e2a260dc80f 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -118,8 +118,8 @@ play_media: media_content_type: name: Content type description: - The type of the content to play. Like image, music, tvshow, - video, episode, channel or playlist. + The type of the content to play. Like image, music, tvshow, video, + episode, channel or playlist. required: true example: "music" selector: @@ -184,3 +184,24 @@ repeat_set: - "off" - "all" - "one" + +join: + description: + Group players together. Only works on platforms with support for player + groups. + name: Join + target: + fields: + group_members: + description: + The players which will be synced with the target player. + example: + - "media_player.multiroom_player2" + - "media_player.multiroom_player3" + +unjoin: + description: + Unjoin the player from a group. Only works on platforms with support for + player groups. + name: Unjoin + target: diff --git a/homeassistant/components/media_player/translations/hu.json b/homeassistant/components/media_player/translations/hu.json index 0eae14fdd98..83b5dc4e122 100644 --- a/homeassistant/components/media_player/translations/hu.json +++ b/homeassistant/components/media_player/translations/hu.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} be van kapcsolva", "is_paused": "{entity_name} sz\u00fcneteltetve van", "is_playing": "{entity_name} lej\u00e1tszik" + }, + "trigger_type": { + "idle": "{entity_name} t\u00e9tlenn\u00e9 v\u00e1lik", + "paused": "{entity_name} sz\u00fcneteltetve van", + "playing": "{entity_name} megkezdi a lej\u00e1tsz\u00e1st", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" } }, "state": { diff --git a/homeassistant/components/media_player/translations/id.json b/homeassistant/components/media_player/translations/id.json index bcf12d72542..e759f88a15a 100644 --- a/homeassistant/components/media_player/translations/id.json +++ b/homeassistant/components/media_player/translations/id.json @@ -1,11 +1,27 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} siaga", + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala", + "is_paused": "{entity_name} dijeda", + "is_playing": "{entity_name} sedang memutar" + }, + "trigger_type": { + "idle": "{entity_name} menjadi siaga", + "paused": "{entity_name} dijeda", + "playing": "{entity_name} mulai memutar", + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan" + } + }, "state": { "_": { - "idle": "Diam", - "off": "Off", - "on": "On", + "idle": "Siaga", + "off": "Mati", + "on": "Nyala", "paused": "Jeda", - "playing": "Memainkan", + "playing": "Memutar", "standby": "Siaga" } }, diff --git a/homeassistant/components/media_player/translations/ko.json b/homeassistant/components/media_player/translations/ko.json index e727e744d73..213b61ef6b9 100644 --- a/homeassistant/components/media_player/translations/ko.json +++ b/homeassistant/components/media_player/translations/ko.json @@ -1,11 +1,18 @@ { "device_automation": { "condition_type": { - "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734 \uc0c1\ud0dc\uc774\uba74", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", - "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74", - "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd \uc911\uc774\uba74" + "is_idle": "{entity_name}\uc774(\uac00) \ub300\uae30 \uc0c1\ud0dc\uc774\uba74", + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_paused": "{entity_name}\uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_playing": "{entity_name}\uc774(\uac00) \uc7ac\uc0dd \uc911\uc774\uba74" + }, + "trigger_type": { + "idle": "{entity_name}\uc774(\uac00) \ub300\uae30 \uc0c1\ud0dc\uac00 \ub420 \ub54c", + "paused": "{entity_name}\uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub420 \ub54c", + "playing": "{entity_name}\uc774(\uac00) \uc7ac\uc0dd\uc744 \uc2dc\uc791\ud560 \ub54c", + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/media_player/translations/zh-Hans.json b/homeassistant/components/media_player/translations/zh-Hans.json index af8579075be..0fa034898c3 100644 --- a/homeassistant/components/media_player/translations/zh-Hans.json +++ b/homeassistant/components/media_player/translations/zh-Hans.json @@ -6,6 +6,13 @@ "is_on": "{entity_name} \u5df2\u5f00\u542f", "is_paused": "{entity_name} \u5df2\u6682\u505c", "is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e" + }, + "trigger_type": { + "idle": "{entity_name} \u7a7a\u95f2", + "paused": "{entity_name} \u6682\u505c", + "playing": "{entity_name} \u5f00\u59cb\u64ad\u653e", + "turned_off": "{entity_name} \u88ab\u5173\u95ed", + "turned_on": "{entity_name} \u88ab\u6253\u5f00" } }, "state": { diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 3dff949d5dd..6aa01403a5f 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -1,6 +1,7 @@ """The media_source integration.""" +from __future__ import annotations + from datetime import timedelta -from typing import Optional import voluptuous as vol @@ -54,7 +55,7 @@ async def _process_media_source_platform(hass, domain, platform): @callback def _get_media_item( - hass: HomeAssistant, media_content_id: Optional[str] + hass: HomeAssistant, media_content_id: str | None ) -> models.MediaSourceItem: """Return media item.""" if media_content_id: diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index fa62ba48c5f..fb5e9094dfb 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -1,7 +1,8 @@ """Local Media Source Implementation.""" +from __future__ import annotations + import mimetypes from pathlib import Path -from typing import Tuple from aiohttp import web @@ -40,7 +41,7 @@ class LocalSource(MediaSource): return Path(self.hass.config.media_dirs[source_dir_id], location) @callback - def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]: + def async_parse_identifier(self, item: MediaSourceItem) -> tuple[str, str]: """Parse identifier.""" if not item.identifier: # Empty source_dir_id and location @@ -69,7 +70,7 @@ class LocalSource(MediaSource): return PlayMedia(f"/media/{item.identifier}", mime_type) async def async_browse_media( - self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES + self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES ) -> BrowseMediaSource: """Return media.""" try: diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 98b817344d9..aa17fff320e 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import ABC from dataclasses import dataclass -from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -28,9 +27,9 @@ class PlayMedia: class BrowseMediaSource(BrowseMedia): """Represent a browsable media file.""" - children: Optional[List["BrowseMediaSource"]] + children: list[BrowseMediaSource] | None - def __init__(self, *, domain: Optional[str], identifier: Optional[str], **kwargs): + def __init__(self, *, domain: str | None, identifier: str | None, **kwargs): """Initialize media source browse media.""" media_content_id = f"{URI_SCHEME}{domain or ''}" if identifier: @@ -47,7 +46,7 @@ class MediaSourceItem: """A parsed media item.""" hass: HomeAssistant - domain: Optional[str] + domain: str | None identifier: str async def async_browse(self) -> BrowseMediaSource: @@ -118,7 +117,7 @@ class MediaSource(ABC): raise NotImplementedError async def async_browse_media( - self, item: MediaSourceItem, media_types: Tuple[str] + self, item: MediaSourceItem, media_types: tuple[str] ) -> BrowseMediaSource: """Browse media.""" raise NotImplementedError diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 0e81d6101b3..0f48db96bf8 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -1,8 +1,10 @@ """The MELCloud Climate integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict, List +from typing import Any from aiohttp import ClientConnectionError from async_timeout import timeout @@ -101,7 +103,7 @@ class MelCloudDevice: _LOGGER.warning("Connection failed for %s", self.name) self._available = False - async def async_set(self, properties: Dict[str, Any]): + async def async_set(self, properties: dict[str, Any]): """Write state changes to the MELCloud API.""" try: await self.device.set(properties) @@ -142,7 +144,7 @@ class MelCloudDevice: return _device_info -async def mel_devices_setup(hass, token) -> List[MelCloudDevice]: +async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: """Query connected devices from MELCloud.""" session = hass.helpers.aiohttp_client.async_get_clientsession() try: diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 4c409ec5a4d..8e45cc3d9a4 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -1,6 +1,8 @@ """Platform for climate integration.""" +from __future__ import annotations + from datetime import timedelta -from typing import Any, Dict, List, Optional +from typing import Any from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW, AtaDevice, AtwDevice import pymelcloud.ata_device as ata @@ -114,7 +116,7 @@ class MelCloudClimate(ClimateEntity): return self.api.device_info @property - def target_temperature_step(self) -> Optional[float]: + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" return self._base_device.temperature_increment @@ -128,7 +130,7 @@ class AtaDeviceClimate(MelCloudClimate): self._device = ata_device @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return f"{self.api.device.serial}-{self.api.device.mac}" @@ -138,7 +140,7 @@ class AtaDeviceClimate(MelCloudClimate): return self._name @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes with device specific additions.""" attr = {} @@ -190,19 +192,19 @@ class AtaDeviceClimate(MelCloudClimate): await self._device.set(props) @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return [HVAC_MODE_OFF] + [ ATA_HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes ] @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.room_temperature @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device.target_temperature @@ -213,7 +215,7 @@ class AtaDeviceClimate(MelCloudClimate): ) @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._device.fan_speed @@ -222,7 +224,7 @@ class AtaDeviceClimate(MelCloudClimate): await self._device.set({"fan_speed": fan_mode}) @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" return self._device.fan_speeds @@ -243,7 +245,7 @@ class AtaDeviceClimate(MelCloudClimate): await self._device.set({ata.PROPERTY_VANE_VERTICAL: position}) @property - def swing_mode(self) -> Optional[str]: + def swing_mode(self) -> str | None: """Return vertical vane position or mode.""" return self._device.vane_vertical @@ -252,7 +254,7 @@ class AtaDeviceClimate(MelCloudClimate): await self.async_set_vane_vertical(swing_mode) @property - def swing_modes(self) -> Optional[str]: + def swing_modes(self) -> str | None: """Return a list of available vertical vane positions and modes.""" return self._device.vane_vertical_positions @@ -300,7 +302,7 @@ class AtwDeviceZoneClimate(MelCloudClimate): self._zone = atw_zone @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return f"{self.api.device.serial}-{self._zone.zone_index}" @@ -310,7 +312,7 @@ class AtwDeviceZoneClimate(MelCloudClimate): return f"{self._name} {self._zone.name}" @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes with device specific additions.""" data = { ATTR_STATUS: ATW_ZONE_HVAC_MODE_LOOKUP.get( @@ -351,17 +353,17 @@ class AtwDeviceZoneClimate(MelCloudClimate): await self._device.set(props) @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return [self.hvac_mode] @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._zone.room_temperature @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._zone.target_temperature diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 41ce24989a5..98ba343bfcb 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -1,6 +1,7 @@ """Config flow for the MELCloud platform.""" +from __future__ import annotations + import asyncio -from typing import Optional from aiohttp import ClientError, ClientResponseError from async_timeout import timeout @@ -16,7 +17,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -37,8 +38,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, username: str, *, - password: Optional[str] = None, - token: Optional[str] = None, + password: str | None = None, + token: str | None = None, ): """Create client.""" if password is None and token is None: diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index c96433f17df..356992ece11 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -2,20 +2,20 @@ from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW from pymelcloud.atw_device import Zone +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from . import MelCloudDevice from .const import DOMAIN ATTR_MEASUREMENT_NAME = "measurement_name" -ATTR_ICON = "icon" ATTR_UNIT = "unit" -ATTR_DEVICE_CLASS = "device_class" ATTR_VALUE_FN = "value_fn" ATTR_ENABLED_FN = "enabled" @@ -110,7 +110,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class MelDeviceSensor(Entity): +class MelDeviceSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, api: MelCloudDevice, measurement, definition): diff --git a/homeassistant/components/melcloud/translations/he.json b/homeassistant/components/melcloud/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/melcloud/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/hu.json b/homeassistant/components/melcloud/translations/hu.json index 62699ecb468..7f81269c700 100644 --- a/homeassistant/components/melcloud/translations/hu.json +++ b/homeassistant/components/melcloud/translations/hu.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/melcloud/translations/id.json b/homeassistant/components/melcloud/translations/id.json new file mode 100644 index 00000000000..d2847541537 --- /dev/null +++ b/homeassistant/components/melcloud/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Integrasi MELCloud sudah dikonfigurasi untuk email ini. Token akses telah disegarkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "description": "Hubungkan menggunakan akun MELCloud Anda.", + "title": "Hubungkan ke MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/ko.json b/homeassistant/components/melcloud/translations/ko.json index 2e1f1b535e1..ce984e26906 100644 --- a/homeassistant/components/melcloud/translations/ko.json +++ b/homeassistant/components/melcloud/translations/ko.json @@ -15,7 +15,7 @@ "username": "\uc774\uba54\uc77c" }, "description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.", - "title": "MELCloud \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "MELCloud\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/melcloud/translations/nl.json b/homeassistant/components/melcloud/translations/nl.json index 8ef8cc716b1..481027f9092 100644 --- a/homeassistant/components/melcloud/translations/nl.json +++ b/homeassistant/components/melcloud/translations/nl.json @@ -4,15 +4,15 @@ "already_configured": "MELCloud integratie is al geconfigureerd voor deze e-mail. Toegangstoken is vernieuwd." }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "password": "MELCloud wachtwoord.", - "username": "E-mail gebruikt om in te loggen op MELCloud." + "password": "Wachtwoord", + "username": "E-mail" }, "description": "Maak verbinding via uw MELCloud account.", "title": "Maak verbinding met MELCloud" diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index ae10b5140f7..e01d78a5270 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -1,5 +1,5 @@ """Platform for water_heater integration.""" -from typing import List, Optional +from __future__ import annotations from pymelcloud import DEVICE_TYPE_ATW, AtwDevice from pymelcloud.atw_device import ( @@ -49,7 +49,7 @@ class AtwWaterHeater(WaterHeaterEntity): await self._api.async_update() @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return f"{self._api.device.serial}" @@ -72,7 +72,7 @@ class AtwWaterHeater(WaterHeaterEntity): await self._device.set({PROPERTY_POWER: False}) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes with device specific additions.""" data = {ATTR_STATUS: self._device.status} return data @@ -83,17 +83,17 @@ class AtwWaterHeater(WaterHeaterEntity): return TEMP_CELSIUS @property - def current_operation(self) -> Optional[str]: + def current_operation(self) -> str | None: """Return current operation as reported by pymelcloud.""" return self._device.operation_mode @property - def operation_list(self) -> List[str]: + def operation_list(self) -> list[str]: """Return the list of available operation modes as reported by pymelcloud.""" return self._device.operation_modes @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._device.tank_temperature @@ -122,11 +122,11 @@ class AtwWaterHeater(WaterHeaterEntity): return SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @property - def min_temp(self) -> Optional[float]: + def min_temp(self) -> float | None: """Return the minimum temperature.""" return self._device.target_tank_temperature_min @property - def max_temp(self) -> Optional[float]: + def max_temp(self) -> float | None: """Return the maximum temperature.""" return self._device.target_tank_temperature_max diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 55186d63146..13644c1d341 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -56,7 +56,7 @@ class MerakiView(HomeAssistantView): return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) _LOGGER.debug("Meraki Data from Post: %s", json.dumps(data)) if not data.get("secret", False): - _LOGGER.error("secret invalid") + _LOGGER.error("The secret is invalid") return self.json_message("No secret", HTTP_UNPROCESSABLE_ENTITY) if data["secret"] != self.secret: _LOGGER.error("Invalid Secret received from Meraki") diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 5a357467920..47d946b92e7 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -14,13 +14,17 @@ from homeassistant.const import ( LENGTH_METERS, ) from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady 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 import homeassistant.util.dt as dt_util -from .const import CONF_TRACK_HOME, DOMAIN +from .const import ( + CONF_TRACK_HOME, + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, +) URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" @@ -36,11 +40,22 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: async def async_setup_entry(hass, config_entry): """Set up Met as config entry.""" - coordinator = MetDataUpdateCoordinator(hass, config_entry) - await coordinator.async_refresh() + # Don't setup if tracking home location and latitude or longitude isn't set. + # Also, filters out our onboarding default location. + if config_entry.data.get(CONF_TRACK_HOME, False) and ( + (not hass.config.latitude and not hass.config.longitude) + or ( + hass.config.latitude == DEFAULT_HOME_LATITUDE + and hass.config.longitude == DEFAULT_HOME_LONGITUDE + ) + ): + _LOGGER.warning( + "Skip setting up met.no integration; No Home location has been set" + ) + return False - if not coordinator.last_update_success: - raise ConfigEntryNotReady + coordinator = MetDataUpdateCoordinator(hass, config_entry) + await coordinator.async_config_entry_first_refresh() if config_entry.data.get(CONF_TRACK_HOME, False): coordinator.track_home() @@ -72,7 +87,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): self.weather = MetWeatherData( hass, config_entry.data, hass.config.units.is_metric ) - self.weather.init_data() + self.weather.set_coordinates() update_interval = timedelta(minutes=randrange(55, 65)) @@ -92,8 +107,8 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_weather_data(_event=None): """Update weather data.""" - self.weather.init_data() - await self.async_refresh() + if self.weather.set_coordinates(): + await self.async_refresh() self._unsub_track_home = self.hass.bus.async_listen( EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data @@ -118,9 +133,10 @@ class MetWeatherData: self.current_weather_data = {} self.daily_forecast = None self.hourly_forecast = None + self._coordinates = None - def init_data(self): - """Weather data inialization - get the coordinates.""" + def set_coordinates(self): + """Weather data inialization - set the coordinates.""" if self._config.get(CONF_TRACK_HOME, False): latitude = self.hass.config.latitude longitude = self.hass.config.longitude @@ -140,10 +156,14 @@ class MetWeatherData: "lon": str(longitude), "msl": str(elevation), } + if coordinates == self._coordinates: + return False + self._coordinates = coordinates self._weather_data = metno.MetWeatherData( coordinates, async_get_clientsession(self.hass), api_url=URL ) + return True async def fetch_data(self): """Fetch data from API - (current weather and forecast).""" diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 6b3d6735f46..5cfd71ea801 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure Met component.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -8,7 +10,13 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, C from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME +from .const import ( + CONF_TRACK_HOME, + DEFAULT_HOME_LATITUDE, + DEFAULT_HOME_LONGITUDE, + DOMAIN, + HOME_LOCATION_NAME, +) @callback @@ -73,14 +81,20 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import( - self, user_input: Optional[Dict] = None - ) -> Dict[str, Any]: + async def async_step_import(self, user_input: dict | None = None) -> dict[str, Any]: """Handle configuration by yaml file.""" return await self.async_step_user(user_input) async def async_step_onboarding(self, data=None): """Handle a flow initialized by onboarding.""" + # Don't create entry if latitude or longitude isn't set. + # Also, filters out our onboarding default location. + if (not self.hass.config.latitude and not self.hass.config.longitude) or ( + self.hass.config.latitude == DEFAULT_HOME_LATITUDE + and self.hass.config.longitude == DEFAULT_HOME_LONGITUDE + ): + return self.async_abort(reason="no_home") + return self.async_create_entry( title=HOME_LOCATION_NAME, data={CONF_TRACK_HOME: True} ) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index b78c412393d..0f4c22dbba3 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -34,6 +34,9 @@ HOME_LOCATION_NAME = "Home" CONF_TRACK_HOME = "track_home" +DEFAULT_HOME_LATITUDE = 52.3731339 +DEFAULT_HOME_LONGITUDE = 4.8903147 + ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}" CONDITIONS_MAP = { diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json index b9e94aba865..b9d251e21d8 100644 --- a/homeassistant/components/met/strings.json +++ b/homeassistant/components/met/strings.json @@ -12,6 +12,11 @@ } } }, - "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "abort": { + "no_home": "No home coordinates are set in the Home Assistant configuration" + } } } diff --git a/homeassistant/components/met/translations/he.json b/homeassistant/components/met/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/met/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/hu.json b/homeassistant/components/met/translations/hu.json index 4e3ccf87ab7..b9141541a93 100644 --- a/homeassistant/components/met/translations/hu.json +++ b/homeassistant/components/met/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met/translations/id.json b/homeassistant/components/met/translations/id.json index 12854e4ed61..639ed5086ce 100644 --- a/homeassistant/components/met/translations/id.json +++ b/homeassistant/components/met/translations/id.json @@ -1,11 +1,17 @@ { "config": { + "error": { + "already_configured": "Layanan sudah dikonfigurasi" + }, "step": { "user": { "data": { "elevation": "Ketinggian", + "latitude": "Lintang", + "longitude": "Bujur", "name": "Nama" }, + "description": "Meteorologisk institutt", "title": "Lokasi" } } diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index c0c8c11c644..4657da9e5d4 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -230,14 +230,13 @@ class MetWeather(CoordinatorEntity, WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } - if not self._is_metric: - if 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 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] diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 3034135f847..1229a4e43af 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool ) else: _LOGGER.warning( - "Weather alert not available: The city %s is not in metropolitan France or Andorre.", + "Weather alert not available: The city %s is not in metropolitan France or Andorre", entry.title, ) @@ -189,7 +189,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): ].data.position.get("dept") hass.data[DOMAIN][department] = False _LOGGER.debug( - "Weather alert for depatment %s unloaded and released. It can be added now by another city.", + "Weather alert for depatment %s unloaded and released. It can be added now by another city", department, ) diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index f4d7c5dccfa..2ea9ed0568c 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -9,8 +9,7 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.core import callback -from .const import CONF_CITY, FORECAST_MODE, FORECAST_MODE_DAILY -from .const import DOMAIN # pylint: disable=unused-import +from .const import CONF_CITY, DOMAIN, FORECAST_MODE, FORECAST_MODE_DAILY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 8de4e76c6f6..6ffcda29229 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": [ - "meteofrance-api==1.0.1" + "meteofrance-api==1.0.2" ], "codeowners": [ "@hacf-fr", diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 201cca7ae9d..b6ec221a97e 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -6,6 +6,7 @@ from meteofrance_api.helpers import ( readeable_phenomenoms_dict, ) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.typing import HomeAssistantType @@ -74,7 +75,7 @@ async def async_setup_entry( ) -class MeteoFranceSensor(CoordinatorEntity): +class MeteoFranceSensor(CoordinatorEntity, SensorEntity): """Representation of a Meteo-France sensor.""" def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator): @@ -154,7 +155,7 @@ class MeteoFranceSensor(CoordinatorEntity): return SENSOR_TYPES[self._type][ENTITY_ENABLE] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -177,7 +178,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" reference_dt = self.coordinator.data.forecast[0]["dt"] return { @@ -193,7 +194,6 @@ class MeteoFranceRainSensor(MeteoFranceSensor): class MeteoFranceAlertSensor(MeteoFranceSensor): """Representation of a Meteo-France alert sensor.""" - # pylint: disable=super-init-not-called def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator): """Initialize the Meteo-France sensor.""" super().__init__(sensor_type, coordinator) @@ -209,7 +209,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { **readeable_phenomenoms_dict(self.coordinator.data.phenomenons_max_colors), diff --git a/homeassistant/components/meteo_france/translations/hu.json b/homeassistant/components/meteo_france/translations/hu.json index dc74eafa409..112f70b6ea6 100644 --- a/homeassistant/components/meteo_france/translations/hu.json +++ b/homeassistant/components/meteo_france/translations/hu.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van", - "unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "empty": "Nincs eredm\u00e9ny a v\u00e1roskeres\u00e9sben: ellen\u0151rizze a v\u00e1ros mez\u0151t" }, "step": { + "cities": { + "data": { + "city": "V\u00e1ros" + }, + "description": "V\u00e1laszd ki a v\u00e1rost a list\u00e1b\u00f3l", + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "V\u00e1ros" @@ -16,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "El\u0151rejelz\u00e9si m\u00f3d" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/id.json b/homeassistant/components/meteo_france/translations/id.json new file mode 100644 index 00000000000..07d8450e873 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "empty": "Tidak ada hasil dalam penelusuran kota: periksa bidang isian kota" + }, + "step": { + "cities": { + "data": { + "city": "Kota" + }, + "description": "Pilih kota Anda dari daftar", + "title": "M\u00e9t\u00e9o-France" + }, + "user": { + "data": { + "city": "Kota" + }, + "description": "Masukkan kode pos (hanya untuk Prancis, disarankan) atau nama kota", + "title": "M\u00e9t\u00e9o-France" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Mode prakiraan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json index ec48103bbff..83cda0e4dcf 100644 --- a/homeassistant/components/meteo_france/translations/ko.json +++ b/homeassistant/components/meteo_france/translations/ko.json @@ -1,14 +1,18 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured": "\uc704\uce58\uac00 \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": { - "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c: \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624." + "empty": "\ub3c4\uc2dc \uac80\uc0c9 \uacb0\uacfc\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \ub3c4\uc2dc \ud544\ub4dc\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "step": { "cities": { + "data": { + "city": "\ub3c4\uc2dc" + }, + "description": "\ubaa9\ub85d\uc5d0\uc11c \ub3c4\uc2dc\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" }, "user": { diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json index 61925da4cd3..f69db3ed47e 100644 --- a/homeassistant/components/meteo_france/translations/nl.json +++ b/homeassistant/components/meteo_france/translations/nl.json @@ -1,11 +1,18 @@ { "config": { "abort": { - "already_configured": "Stad al geconfigureerd", - "unknown": "Onbekende fout: probeer het later nog eens" + "already_configured": "Locatie is al geconfigureerd.", + "unknown": "Onverwachte fout" + }, + "error": { + "empty": "Geen resultaat bij het zoeken naar een stad: controleer de invoer: stad" }, "step": { "cities": { + "data": { + "city": "Stad" + }, + "description": "Kies uw stad uit de lijst", "title": "M\u00e9t\u00e9o-France" }, "user": { @@ -16,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Voorspellingsmodus" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 09e062cc715..08d5c1c4f6a 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -59,7 +59,7 @@ async def async_setup_entry( True, ) _LOGGER.debug( - "Weather entity (%s) added for %s.", + "Weather entity (%s) added for %s", entry.options.get(CONF_MODE, FORECAST_MODE_DAILY), coordinator.data.position["name"], ) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 6b13d03ebba..6d237c696f6 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -73,7 +73,7 @@ class MeteoAlertBinarySensor(BinarySensorEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return self._attributes diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 8a68646240a..87a5488fe01 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -61,9 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if metoffice_data.now is None: raise ConfigEntryNotReady() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -74,8 +74,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index b71c3de67e3..7dd3788f8b7 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers import config_validation as cv -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index aed763ca4a4..6a7cf5254a6 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,4 +1,5 @@ """Support for UK Met Office weather service.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_HUMIDITY, @@ -10,7 +11,6 @@ from homeassistant.const import ( UV_INDEX, ) from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( @@ -92,7 +92,7 @@ async def async_setup_entry( ) -class MetOfficeCurrentSensor(Entity): +class MetOfficeCurrentSensor(SensorEntity): """Implementation of a Met Office current weather condition sensor.""" def __init__(self, entry_data, hass_data, sensor_type): @@ -171,7 +171,7 @@ class MetOfficeCurrentSensor(Entity): return SENSOR_TYPES[self._type][1] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/metoffice/translations/he.json b/homeassistant/components/metoffice/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/metoffice/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/hu.json b/homeassistant/components/metoffice/translations/hu.json index 3b2d79a34a7..350e6f92f32 100644 --- a/homeassistant/components/metoffice/translations/hu.json +++ b/homeassistant/components/metoffice/translations/hu.json @@ -1,7 +1,21 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + }, + "title": "Csatlakoz\u00e1s a UK Met Office-hoz" + } } } } \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/id.json b/homeassistant/components/metoffice/translations/id.json new file mode 100644 index 00000000000..d9bb784a99f --- /dev/null +++ b/homeassistant/components/metoffice/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur" + }, + "description": "Lintang dan bujur akan digunakan untuk menemukan stasiun cuaca terdekat.", + "title": "Hubungkan ke the UK Met Office" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/nl.json b/homeassistant/components/metoffice/translations/nl.json index 1b3063459c4..a6ba36f07af 100644 --- a/homeassistant/components/metoffice/translations/nl.json +++ b/homeassistant/components/metoffice/translations/nl.json @@ -14,6 +14,7 @@ "latitude": "Breedtegraad", "longitude": "Lengtegraad" }, + "description": "De lengte- en breedtegraad worden gebruikt om het dichtstbijzijnde weerstation te vinden.", "title": "Maak verbinding met het UK Met Office" } } diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 671a52bbf01..c7a64f17bd6 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -5,7 +5,7 @@ from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -18,7 +18,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -74,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class MfiSensor(Entity): +class MfiSensor(SensorEntity): """Representation of a mFi sensor.""" def __init__(self, port, hass): diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 21963140547..150f81298cd 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -107,7 +107,7 @@ class MfiSwitch(SwitchEntity): return int(self._port.data.get("active_pwr", 0)) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes for the device.""" return { "volts": round(self._port.data.get("v_rms", 0), 1), diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index e77f17c9140..0f0735dd5da 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -5,7 +5,7 @@ import logging from pmsensor import co2sensor import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_TEMPERATURE, CONCENTRATION_PARTS_PER_MILLION, @@ -14,7 +14,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -69,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class MHZ19Sensor(Entity): +class MHZ19Sensor(SensorEntity): """Representation of an CO2 sensor.""" def __init__(self, mhz_client, sensor_type, temp_unit, name): @@ -107,7 +106,7 @@ class MHZ19Sensor(Entity): self._ppm = data.get(SENSOR_CO2) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" result = {} if self._sensor_type == SENSOR_TEMPERATURE and self._ppm is not None: diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index b9046429603..9f7131d1935 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -231,7 +231,7 @@ class MicrosoftFaceGroupEntity(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" attr = {} for name, p_id in self._api.store[self._id].items(): diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 6583f5f7e0c..0c22a943bb3 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -5,10 +5,10 @@ import logging import btlewrap from btlewrap import BluetoothBackendException -from miflora import miflora_poller # pylint: disable=import-error +from miflora import miflora_poller import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONDUCTIVITY, CONF_FORCE_UPDATE, @@ -27,7 +27,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util from homeassistant.util.temperature import celsius_to_fahrenheit @@ -130,7 +129,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devs) -class MiFloraSensor(Entity): +class MiFloraSensor(SensorEntity): """Implementing the MiFlora sensor.""" def __init__( @@ -189,7 +188,7 @@ class MiFloraSensor(Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update} diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index b9e0b051aba..025eff8d07a 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -123,7 +123,7 @@ class MikrotikHubTracker(ScannerEntity): return self.hub.available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if self.is_connected: return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 28a78d0ee1a..2f1f89ba60d 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -276,9 +276,8 @@ class MikrotikData: def update(self): """Update device_tracker from Mikrotik API.""" - if not self.available or not self.api: - if not self.connect_to_hub(): - return + if (not self.available or not self.api) and not self.connect_to_hub(): + return _LOGGER.debug("updating network devices for host: %s", self._host) self.update_devices() diff --git a/homeassistant/components/mikrotik/translations/he.json b/homeassistant/components/mikrotik/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/mikrotik/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/hu.json b/homeassistant/components/mikrotik/translations/hu.json index 67a2e8d8fc3..248884f9687 100644 --- a/homeassistant/components/mikrotik/translations/hu.json +++ b/homeassistant/components/mikrotik/translations/hu.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "A Mikrotik m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "A kapcsolat sikertelen", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" }, "step": { diff --git a/homeassistant/components/mikrotik/translations/id.json b/homeassistant/components/mikrotik/translations/id.json new file mode 100644 index 00000000000..3ef0dacb763 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "name_exists": "Nama sudah ada" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna", + "verify_ssl": "Gunakan SSL" + }, + "title": "Siapkan Router Mikrotik" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Aktifkan ping ARP", + "detection_time": "Pertimbangkan interval rumah", + "force_dhcp": "Paksa pemindaian menggunakan DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/nl.json b/homeassistant/components/mikrotik/translations/nl.json index 53e05b5cf5f..78e143ddadb 100644 --- a/homeassistant/components/mikrotik/translations/nl.json +++ b/homeassistant/components/mikrotik/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Mikrotik is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding niet geslaagd", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "name_exists": "Naam bestaat al" }, diff --git a/homeassistant/components/mikrotik/translations/ru.json b/homeassistant/components/mikrotik/translations/ru.json index 21391f12b1c..06e9d647545 100644 --- a/homeassistant/components/mikrotik/translations/ru.json +++ b/homeassistant/components/mikrotik/translations/ru.json @@ -15,7 +15,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL" }, "title": "MikroTik" diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 0bb94242d64..7a1adc6a0bc 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -107,7 +107,7 @@ class MillHeater(ClimateEntity): return self._heater.name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" res = { "open_window": self._heater.open_window, diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py index f1b94ab2ac8..dead7a1ff9d 100644 --- a/homeassistant/components/mill/config_flow.py +++ b/homeassistant/components/mill/config_flow.py @@ -6,7 +6,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN DATA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} diff --git a/homeassistant/components/mill/translations/hu.json b/homeassistant/components/mill/translations/hu.json index 387a73041d9..74a6f9abbac 100644 --- a/homeassistant/components/mill/translations/hu.json +++ b/homeassistant/components/mill/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mill/translations/id.json b/homeassistant/components/mill/translations/id.json new file mode 100644 index 00000000000..ab929d3d7c8 --- /dev/null +++ b/homeassistant/components/mill/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json index db2d4651c2d..eac6c63c559 100644 --- a/homeassistant/components/mill/translations/ru.json +++ b/homeassistant/components/mill/translations/ru.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index d2ffb9f5ec0..d103ff8eaa6 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -13,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service @@ -85,9 +84,10 @@ def calc_min(sensor_values): val = None entity_id = None for sensor_id, sensor_value in sensor_values: - if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: - if val is None or val > sensor_value: - entity_id, val = sensor_id, sensor_value + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and ( + val is None or val > sensor_value + ): + entity_id, val = sensor_id, sensor_value return entity_id, val @@ -96,30 +96,35 @@ def calc_max(sensor_values): val = None entity_id = None for sensor_id, sensor_value in sensor_values: - if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: - if val is None or val < sensor_value: - entity_id, val = sensor_id, sensor_value + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and ( + val is None or val < sensor_value + ): + entity_id, val = sensor_id, sensor_value return entity_id, val def calc_mean(sensor_values, round_digits): """Calculate mean value, honoring unknown states.""" - result = [] - for _, sensor_value in sensor_values: - if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: - result.append(sensor_value) - if len(result) == 0: + result = [ + sensor_value + for _, sensor_value in sensor_values + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ] + + if not result: return None return round(sum(result) / len(result), round_digits) def calc_median(sensor_values, round_digits): """Calculate median value, honoring unknown states.""" - result = [] - for _, sensor_value in sensor_values: - if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: - result.append(sensor_value) - if len(result) == 0: + result = [ + sensor_value + for _, sensor_value in sensor_values + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ] + + if not result: return None result.sort() if len(result) % 2 == 0: @@ -131,7 +136,7 @@ def calc_median(sensor_values, round_digits): return round(median, round_digits) -class MinMaxSensor(Entity): +class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" def __init__(self, entity_ids, name, sensor_type, round_digits): @@ -188,7 +193,7 @@ class MinMaxSensor(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return { attr: getattr(self, attr) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 164bb264f90..f76e8e8467e 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,9 +1,10 @@ """The Minecraft Server integration.""" +from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Dict +from typing import Any from mcstatus.server import MinecraftServer as MCStatus @@ -246,7 +247,7 @@ class MinecraftServerEntity(Entity): "sw_version": self._server.protocol_version, } self._device_class = device_class - self._device_state_attributes = None + self._extra_state_attributes = None self._disconnect_dispatcher = None @property @@ -260,7 +261,7 @@ class MinecraftServerEntity(Entity): return self._unique_id @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information.""" return self._device_info diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py index a7cb0371f67..b252a712153 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Minecraft Server integration.""" +from contextlib import suppress from functools import partial import ipaddress @@ -10,12 +11,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from . import MinecraftServer, helpers -from .const import ( # pylint: disable=unused-import - DEFAULT_HOST, - DEFAULT_NAME, - DEFAULT_PORT, - DOMAIN, -) +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DOMAIN class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): @@ -40,10 +36,8 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): host = address_right else: host = address_left - try: + with suppress(ValueError): port = int(address_right) - except ValueError: - pass # 'port' is already set to default value. # Remove '[' and ']' in case of an IPv6 address. host = host.strip("[]") diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index 7f9380cdec2..f6409ce525d 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -1,6 +1,7 @@ """Helper functions for the Minecraft Server integration.""" +from __future__ import annotations -from typing import Any, Dict +from typing import Any import aiodns @@ -10,7 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType 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: HomeAssistantType, 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 03710520b90..2c4a2ae4b8e 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -3,7 +3,7 @@ "name": "Minecraft Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/minecraft_server", - "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==2.3.0"], + "requirements": ["aiodns==2.0.0", "getmac==0.8.2", "mcstatus==5.1.1"], "codeowners": ["@elmurato"], "quality_scale": "silver" } diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 171ff9d1701..3d77d9e2772 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -1,6 +1,9 @@ """The Minecraft Server sensor platform.""" -from typing import Any, Dict +from __future__ import annotations +from typing import Any + +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MILLISECONDS from homeassistant.helpers.typing import HomeAssistantType @@ -45,7 +48,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class MinecraftServerSensorEntity(MinecraftServerEntity): +class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): """Representation of a Minecraft Server sensor base entity.""" def __init__( @@ -141,19 +144,18 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): """Update online players state and device state attributes.""" self._state = self._server.players_online - device_state_attributes = None + extra_state_attributes = None players_list = self._server.players_list - if players_list is not None: - if len(players_list) != 0: - device_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} + if players_list is not None and len(players_list) != 0: + extra_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} - self._device_state_attributes = device_state_attributes + self._extra_state_attributes = extra_state_attributes @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return players list in device state attributes.""" - return self._device_state_attributes + return self._extra_state_attributes class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 7a8958bd7c6..247c1ffc1c3 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van." + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa." diff --git a/homeassistant/components/minecraft_server/translations/id.json b/homeassistant/components/minecraft_server/translations/id.json new file mode 100644 index 00000000000..fffb0865b24 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung ke server. Periksa host dan port lalu coba lagi. Pastikan juga Anda menjalankan Minecraft dengan versi minimal 1.7 di server Anda.", + "invalid_ip": "Alamat IP tidak valid (alamat MAC tidak dapat ditentukan). Perbaiki, lalu coba lagi.", + "invalid_port": "Port harus berada dalam rentang dari 1024 hingga 65535. Perbaiki, lalu coba lagi." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama" + }, + "description": "Siapkan instans Minecraft Server Anda untuk pemantauan.", + "title": "Tautkan Server Minecraft Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/nl.json b/homeassistant/components/minecraft_server/translations/nl.json index 1d589f942e7..0170964d5b0 100644 --- a/homeassistant/components/minecraft_server/translations/nl.json +++ b/homeassistant/components/minecraft_server/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Host is al geconfigureerd." + "already_configured": "Service is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert.", diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index 178058986cc..6e7174b60ee 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -1,9 +1,10 @@ """Minio component.""" +from __future__ import annotations + import logging import os from queue import Queue import threading -from typing import List import voluptuous as vol @@ -230,7 +231,7 @@ class MinioListener: bucket_name: str, prefix: str, suffix: str, - events: List[str], + events: list[str], ): """Create Listener.""" self._queue = queue diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 2aaba9d4085..f2d86067552 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,4 +1,6 @@ """Minio helper methods.""" +from __future__ import annotations + from collections.abc import Iterable import json import logging @@ -6,7 +8,7 @@ from queue import Queue import re import threading import time -from typing import Iterator, List +from typing import Iterator from urllib.parse import unquote from minio import Minio @@ -38,7 +40,7 @@ def create_minio_client( def get_minio_notification_response( - minio_client, bucket_name: str, prefix: str, suffix: str, events: List[str] + minio_client, bucket_name: str, prefix: str, suffix: str, events: list[str] ): """Start listening to minio events. Copied from minio-py.""" query = {"prefix": prefix, "suffix": suffix, "events": events} @@ -87,7 +89,7 @@ class MinioEventThread(threading.Thread): bucket_name: str, prefix: str, suffix: str, - events: List[str], + events: list[str], ): """Copy over all Minio client options.""" super().__init__() diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 244a0c410d5..670a6daf3d3 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -6,7 +6,7 @@ from btlewrap.base import BluetoothBackendException from mitemp_bt import mitemp_bt_poller import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_MAC, @@ -20,7 +20,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity try: import bluepy.btle # noqa: F401 pylint: disable=unused-import @@ -104,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class MiTempBtSensor(Entity): +class MiTempBtSensor(SensorEntity): """Implementing the MiTempBt sensor.""" def __init__(self, poller, parameter, device, name, unit, force_update, median): diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 1a8b1327093..d5008d1778c 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -98,9 +98,12 @@ class MjpegCamera(Camera): self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) self._auth = None - if self._username and self._password: - if self._authentication == HTTP_BASIC_AUTHENTICATION: - self._auth = aiohttp.BasicAuth(self._username, password=self._password) + if ( + self._username + and self._password + and self._authentication == HTTP_BASIC_AUTHENTICATION + ): + self._auth = aiohttp.BasicAuth(self._username, password=self._password) self._verify_ssl = device_info.get(CONF_VERIFY_SSL) async def async_camera_image(self): @@ -144,8 +147,6 @@ class MjpegCamera(Camera): else: req = requests.get(self._mjpeg_url, stream=True, timeout=10) - # https://github.com/PyCQA/pylint/issues/1437 - # pylint: disable=no-member with closing(req) as response: return extract_image_from_mjpeg(response.iter_content(102400)) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 54fa3398ee2..e63698d3eb5 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,17 +1,17 @@ """Integrates Native Apps to Home Assistant.""" import asyncio +from contextlib import suppress from homeassistant.components import cloud, notify as hass_notify from homeassistant.components.webhook import ( async_register as webhook_register, async_unregister as webhook_unregister, ) -from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( - ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, @@ -52,12 +52,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): hass.http.register_view(RegistrationsView()) for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]: - try: + with suppress(ValueError): webhook_register( hass, DOMAIN, "Deleted Webhook", deleted_id, handle_webhook ) - except ValueError: - pass hass.async_create_task( discovery.async_load_platform(hass, "notify", DOMAIN, {}, config) @@ -105,8 +103,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -130,7 +128,5 @@ async def async_remove_entry(hass, entry): await store.async_save(savable_state(hass)) if CONF_CLOUDHOOK_URL in entry.data: - try: + with suppress(cloud.CloudNotAvailable): await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) - except cloud.CloudNotAvailable: - pass diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 36897dd9f69..616cd97a775 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,6 +1,4 @@ """Binary sensor platform for mobile_app.""" -from functools import partial - from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON from homeassistant.core import callback @@ -48,7 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) @callback - def handle_sensor_registration(webhook_id, data): + def handle_sensor_registration(data): if data[CONF_WEBHOOK_ID] != webhook_id: return @@ -66,7 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, f"{DOMAIN}_{ENTITY_TYPE}_register", - partial(handle_sensor_registration, webhook_id), + handle_sensor_registration, ) diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 80b6c8db5e1..752ef86d68d 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -3,9 +3,10 @@ import uuid from homeassistant import config_entries from homeassistant.components import person +from homeassistant.const import ATTR_DEVICE_ID from homeassistant.helpers import entity_registry as er -from .const import ATTR_APP_ID, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN +from .const import ATTR_APP_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index b603e117c4c..af828ce423e 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -20,7 +20,6 @@ ATTR_APP_ID = "app_id" ATTR_APP_NAME = "app_name" ATTR_APP_VERSION = "app_version" ATTR_CONFIG_ENTRY_ID = "entry_id" -ATTR_DEVICE_ID = "device_id" ATTR_DEVICE_NAME = "device_name" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py index 2592d4b486b..33a7510da21 100644 --- a/homeassistant/components/mobile_app/device_action.py +++ b/homeassistant/components/mobile_app/device_action.py @@ -1,5 +1,5 @@ """Provides device actions for Mobile App.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -22,7 +22,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Mobile App devices.""" webhook_id = webhook_id_from_device_id(hass, device_id) @@ -33,7 +33,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" webhook_id = webhook_id_from_device_id(hass, config[CONF_DEVICE_ID]) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index d2e987066ef..1b006f69827 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -7,14 +7,18 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_DEVICE_ID, + ATTR_LATITUDE, + ATTR_LONGITUDE, +) from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_ALTITUDE, ATTR_COURSE, - ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, @@ -52,7 +56,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): return self._data.get(ATTR_BATTERY) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific attributes.""" attrs = {} for key in ATTR_KEYS: diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 748f680da5e..46f4589fa2c 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -34,13 +34,14 @@ class MobileAppEntity(RestoreEntity): self._registration = entry.data self._unique_id = config[CONF_UNIQUE_ID] self._entity_type = config[ATTR_SENSOR_TYPE] - self.unsub_dispatcher = None self._name = config[CONF_NAME] async def async_added_to_hass(self): """Register callbacks.""" - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + ) ) state = await self.async_get_last_state() @@ -49,11 +50,6 @@ class MobileAppEntity(RestoreEntity): self.async_restore_last_state(state) - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - if self.unsub_dispatcher is not None: - self.unsub_dispatcher() - @callback def async_restore_last_state(self, last_state): """Restore previous state.""" @@ -81,7 +77,7 @@ class MobileAppEntity(RestoreEntity): return self._config.get(ATTR_SENSOR_DEVICE_CLASS) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._config[ATTR_SENSOR_ATTRIBUTES] diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index a9079be4f04..63d638cd9e5 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -1,13 +1,20 @@ """Helpers for mobile_app.""" +from __future__ import annotations + import json import logging -from typing import Callable, Dict, Tuple +from typing import Callable from aiohttp.web import Response, json_response from nacl.encoding import Base64Encoder from nacl.secret import SecretBox -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_BAD_REQUEST, HTTP_OK +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONTENT_TYPE_JSON, + HTTP_BAD_REQUEST, + HTTP_OK, +) from homeassistant.core import Context from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.typing import HomeAssistantType @@ -17,7 +24,6 @@ from .const import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_APP_VERSION, - ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, @@ -32,7 +38,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def setup_decrypt() -> Tuple[int, Callable]: +def setup_decrypt() -> tuple[int, Callable]: """Return decryption function and length of key. Async friendly. @@ -45,7 +51,7 @@ def setup_decrypt() -> Tuple[int, Callable]: return (SecretBox.KEY_SIZE, decrypt) -def setup_encrypt() -> Tuple[int, Callable]: +def setup_encrypt() -> tuple[int, Callable]: """Return encryption function and length of key. Async friendly. @@ -58,7 +64,7 @@ def setup_encrypt() -> Tuple[int, Callable]: return (SecretBox.KEY_SIZE, encrypt) -def _decrypt_payload(key: str, ciphertext: str) -> Dict[str, str]: +def _decrypt_payload(key: str, ciphertext: str) -> dict[str, str]: """Decrypt encrypted payload.""" try: keylen, decrypt = setup_decrypt() @@ -84,12 +90,12 @@ def _decrypt_payload(key: str, ciphertext: str) -> Dict[str, str]: return None -def registration_context(registration: Dict) -> Context: +def registration_context(registration: dict) -> Context: """Generate a context from a request.""" return Context(user_id=registration[CONF_USER_ID]) -def empty_okay_response(headers: Dict = None, status: int = HTTP_OK) -> Response: +def empty_okay_response(headers: dict = None, status: int = HTTP_OK) -> Response: """Return a Response with empty JSON object and a 200.""" return Response( text="{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers @@ -117,7 +123,7 @@ def supports_encryption() -> bool: return False -def safe_registration(registration: Dict) -> Dict: +def safe_registration(registration: dict) -> dict: """Return a registration without sensitive values.""" # Sensitive values: webhook_id, secret, cloudhook_url return { @@ -133,7 +139,7 @@ def safe_registration(registration: Dict) -> Dict: } -def savable_state(hass: HomeAssistantType) -> Dict: +def savable_state(hass: HomeAssistantType) -> dict: """Return a clean object containing things that should be saved.""" return { DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS], @@ -141,7 +147,7 @@ def savable_state(hass: HomeAssistantType) -> Dict: def webhook_response( - data, *, registration: Dict, status: int = HTTP_OK, headers: Dict = None + data, *, registration: dict, status: int = HTTP_OK, headers: dict = None ) -> Response: """Return a encrypted response if registration supports it.""" data = json.dumps(data, cls=JSONEncoder) @@ -161,7 +167,7 @@ def webhook_response( ) -def device_info(registration: Dict) -> Dict: +def device_info(registration: dict) -> dict: """Return the device info for this registration.""" return { "identifiers": {(DOMAIN, registration[ATTR_DEVICE_ID])}, diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index a5a96b83bc6..63bf13bad5e 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,6 +1,8 @@ """Provides an HTTP API for mobile_app.""" +from __future__ import annotations + +from contextlib import suppress import secrets -from typing import Dict from aiohttp.web import Request, Response import emoji @@ -9,7 +11,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED +from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID, HTTP_CREATED from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify @@ -18,7 +20,6 @@ from .const import ( ATTR_APP_ID, ATTR_APP_NAME, ATTR_APP_VERSION, - ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, @@ -59,7 +60,7 @@ class RegistrationsView(HomeAssistantView): extra=vol.REMOVE_EXTRA, ) ) - async def post(self, request: Request, data: Dict) -> Response: + async def post(self, request: Request, data: dict) -> Response: """Handle the POST request for registration.""" hass = request.app["hass"] @@ -98,10 +99,8 @@ class RegistrationsView(HomeAssistantView): ) remote_ui_url = None - try: + with suppress(hass.components.cloud.CloudNotAvailable): remote_ui_url = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass return self.json( { diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 46a34fa7a85..803f00764e7 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -84,17 +84,16 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): async def async_get_service(hass, config, discovery_info=None): """Get the mobile_app notification service.""" - session = async_get_clientsession(hass) - service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session) + service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass) return service class MobileAppNotificationService(BaseNotificationService): """Implement the notification service for mobile_app.""" - def __init__(self, session): + def __init__(self, hass): """Initialize the service.""" - self._session = session + self._hass = hass @property def targets(self): @@ -105,10 +104,12 @@ class MobileAppNotificationService(BaseNotificationService): """Send a message to the Lambda APNS gateway.""" data = {ATTR_MESSAGE: message} - if kwargs.get(ATTR_TITLE) is not None: - # Remove default title from notifications. - if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT: - data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) + # Remove default title from notifications. + if ( + kwargs.get(ATTR_TITLE) is not None + and kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT + ): + data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) targets = kwargs.get(ATTR_TARGET) @@ -139,7 +140,9 @@ class MobileAppNotificationService(BaseNotificationService): try: with async_timeout.timeout(10): - response = await self._session.post(push_url, json=data) + response = await async_get_clientsession(self._hass).post( + push_url, json=data + ) result = await response.json() if response.status in [HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED]: diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b09ef86453b..7e3c1c13148 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,5 @@ """Sensor platform for mobile_app.""" -from functools import partial - +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback from homeassistant.helpers import entity_registry as er @@ -49,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) @callback - def handle_sensor_registration(webhook_id, data): + def handle_sensor_registration(data): if data[CONF_WEBHOOK_ID] != webhook_id: return @@ -67,11 +66,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, f"{DOMAIN}_{ENTITY_TYPE}_register", - partial(handle_sensor_registration, webhook_id), + handle_sensor_registration, ) -class MobileAppSensor(MobileAppEntity): +class MobileAppSensor(MobileAppEntity, SensorEntity): """Representation of an mobile app sensor.""" @property diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json index b18f3e7265c..9e388ebc76c 100644 --- a/homeassistant/components/mobile_app/strings.json +++ b/homeassistant/components/mobile_app/strings.json @@ -1,4 +1,5 @@ { + "title": "Mobile App", "config": { "step": { "confirm": { diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index a36fd1ca13a..70709d1be64 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Envia una notificaci\u00f3" } - } + }, + "title": "Aplicaci\u00f3 m\u00f2bil" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/de.json b/homeassistant/components/mobile_app/translations/de.json index 493ceb4dfd1..721cbc09f8d 100644 --- a/homeassistant/components/mobile_app/translations/de.json +++ b/homeassistant/components/mobile_app/translations/de.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Sende eine Benachrichtigung" } - } + }, + "title": "Mobile App" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/en.json b/homeassistant/components/mobile_app/translations/en.json index 34631f86afa..0b564a38174 100644 --- a/homeassistant/components/mobile_app/translations/en.json +++ b/homeassistant/components/mobile_app/translations/en.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Send a notification" } - } + }, + "title": "Mobile App" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json index 43ba004ac72..8ac5c909e17 100644 --- a/homeassistant/components/mobile_app/translations/es.json +++ b/homeassistant/components/mobile_app/translations/es.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Enviar una notificaci\u00f3n" } - } + }, + "title": "Aplicaci\u00f3n m\u00f3vil" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json index 27f5fce2bdf..e5b4ea8009c 100644 --- a/homeassistant/components/mobile_app/translations/et.json +++ b/homeassistant/components/mobile_app/translations/et.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Saada teavitus" } - } + }, + "title": "Mobiilirakendus" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/fr.json b/homeassistant/components/mobile_app/translations/fr.json index f4b0f590e48..f888b8062f8 100644 --- a/homeassistant/components/mobile_app/translations/fr.json +++ b/homeassistant/components/mobile_app/translations/fr.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Envoyer une notification" } - } + }, + "title": "Application mobile" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json index 301075e0ad4..90690e2545b 100644 --- a/homeassistant/components/mobile_app/translations/hu.json +++ b/homeassistant/components/mobile_app/translations/hu.json @@ -13,5 +13,6 @@ "action_type": { "notify": "\u00c9rtes\u00edt\u00e9s k\u00fcld\u00e9se" } - } + }, + "title": "Mobil alkalmaz\u00e1s" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/id.json b/homeassistant/components/mobile_app/translations/id.json new file mode 100644 index 00000000000..d346ca76eab --- /dev/null +++ b/homeassistant/components/mobile_app/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "install_app": "Buka aplikasi seluler untuk menyiapkan integrasi dengan Home Assistant. Baca [dokumentasi]({apps_url}) tentang daftar aplikasi yang kompatibel." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan komponen Aplikasi Seluler?" + } + } + }, + "device_automation": { + "action_type": { + "notify": "Kirim notifikasi" + } + }, + "title": "Aplikasi Seluler" +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/it.json b/homeassistant/components/mobile_app/translations/it.json index f5ba52b1a53..3784a158930 100644 --- a/homeassistant/components/mobile_app/translations/it.json +++ b/homeassistant/components/mobile_app/translations/it.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Invia una notifica" } - } + }, + "title": "App per dispositivi mobili" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/ko.json b/homeassistant/components/mobile_app/translations/ko.json index 03478e1cf2a..7f99e1f2100 100644 --- a/homeassistant/components/mobile_app/translations/ko.json +++ b/homeassistant/components/mobile_app/translations/ko.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "install_app": "Mobile App\uc744 \uc5f4\uc5b4 Home Assistant\uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uad00\ub828 \ubb38\uc11c]({apps_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "confirm": { - "description": "\ubaa8\ubc14\uc77c \uc571 \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "Mobile App \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } - } + }, + "device_automation": { + "action_type": { + "notify": "\uc54c\ub9bc \ubcf4\ub0b4\uae30" + } + }, + "title": "Mobile App" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/nl.json b/homeassistant/components/mobile_app/translations/nl.json index 17a20705cd4..e7bfb6150a5 100644 --- a/homeassistant/components/mobile_app/translations/nl.json +++ b/homeassistant/components/mobile_app/translations/nl.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Stuur een notificatie" } - } + }, + "title": "Mobiele app" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json index 65d465723b1..0f6f6dbb0fd 100644 --- a/homeassistant/components/mobile_app/translations/no.json +++ b/homeassistant/components/mobile_app/translations/no.json @@ -13,5 +13,6 @@ "action_type": { "notify": "Send et varsel" } - } + }, + "title": "Mobilapp" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/pl.json b/homeassistant/components/mobile_app/translations/pl.json index cd083447634..00a4631684c 100644 --- a/homeassistant/components/mobile_app/translations/pl.json +++ b/homeassistant/components/mobile_app/translations/pl.json @@ -13,5 +13,6 @@ "action_type": { "notify": "wy\u015blij powiadomienie" } - } + }, + "title": "Aplikacja mobilna" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/ru.json b/homeassistant/components/mobile_app/translations/ru.json index fc4496ba1d8..65b6cc15c65 100644 --- a/homeassistant/components/mobile_app/translations/ru.json +++ b/homeassistant/components/mobile_app/translations/ru.json @@ -13,5 +13,6 @@ "action_type": { "notify": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435" } - } + }, + "title": "\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/zh-Hans.json b/homeassistant/components/mobile_app/translations/zh-Hans.json index b48ca1e4263..6a884b156bc 100644 --- a/homeassistant/components/mobile_app/translations/zh-Hans.json +++ b/homeassistant/components/mobile_app/translations/zh-Hans.json @@ -8,5 +8,11 @@ "description": "\u60a8\u60f3\u8981\u914d\u7f6e\u79fb\u52a8\u5e94\u7528\u7a0b\u5e8f\u7ec4\u4ef6\u5417\uff1f" } } - } + }, + "device_automation": { + "action_type": { + "notify": "\u63a8\u9001\u901a\u77e5" + } + }, + "title": "\u79fb\u52a8\u5e94\u7528" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/zh-Hant.json b/homeassistant/components/mobile_app/translations/zh-Hant.json index d54afd94f54..2793794b8b6 100644 --- a/homeassistant/components/mobile_app/translations/zh-Hant.json +++ b/homeassistant/components/mobile_app/translations/zh-Hant.json @@ -13,5 +13,6 @@ "action_type": { "notify": "\u50b3\u9001\u901a\u77e5" } - } + }, + "title": "\u624b\u6a5f App" } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index 60dfe242e04..cd4b7c22939 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -1,5 +1,7 @@ """Mobile app utility functions.""" -from typing import TYPE_CHECKING, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING from homeassistant.core import callback @@ -18,7 +20,7 @@ if TYPE_CHECKING: @callback -def webhook_id_from_device_id(hass, device_id: str) -> Optional[str]: +def webhook_id_from_device_id(hass, device_id: str) -> str | None: """Get webhook ID from device ID.""" if DOMAIN not in hass.data: return None @@ -39,9 +41,9 @@ def supports_push(hass, webhook_id: str) -> bool: @callback -def get_notify_service(hass, webhook_id: str) -> Optional[str]: +def get_notify_service(hass, webhook_id: str) -> str | None: """Return the notify service for this webhook ID.""" - notify_service: "MobileAppNotificationService" = hass.data[DOMAIN][DATA_NOTIFY] + notify_service: MobileAppNotificationService = hass.data[DOMAIN][DATA_NOTIFY] for target_service, target_webhook_id in notify_service.registered_targets.items(): if target_webhook_id == webhook_id: diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 3044f2df212..6be39f34f00 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,5 +1,6 @@ """Webhook handlers for mobile_app.""" import asyncio +from contextlib import suppress from functools import wraps import logging import secrets @@ -23,6 +24,7 @@ from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( + ATTR_DEVICE_ID, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, @@ -49,7 +51,6 @@ from .const import ( ATTR_APP_VERSION, ATTR_CAMERA_ENTITY_ID, ATTR_COURSE, - ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE, @@ -471,6 +472,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] resp = {} + for sensor in data: entity_type = sensor[ATTR_SENSOR_TYPE] @@ -494,8 +496,6 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - entry = {CONF_WEBHOOK_ID: config_entry.data[CONF_WEBHOOK_ID]} - try: sensor = sensor_schema_full(sensor) except vol.Invalid as err: @@ -512,9 +512,8 @@ async def webhook_update_sensor_states(hass, config_entry, data): } continue - new_state = {**entry, **sensor} - - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) + sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor) resp[unique_id] = {"success": True} @@ -551,10 +550,8 @@ async def webhook_get_config(hass, config_entry, data): if CONF_CLOUDHOOK_URL in config_entry.data: resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL] - try: + with suppress(hass.components.cloud.CloudNotAvailable): resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass return webhook_response(resp, registration=config_entry.data) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 428ddfadb14..98b1b170905 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,11 +1,25 @@ """Support for Modbus.""" +from typing import Any, Union + import voluptuous as vol +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, +) from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, +) +from homeassistant.components.switch import ( + DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA, +) from homeassistant.const import ( ATTR_STATE, + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, CONF_COVERS, CONF_DELAY, CONF_DEVICE_CLASS, @@ -19,6 +33,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_TIMEOUT, CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, ) import homeassistant.helpers.config_validation as cv @@ -28,11 +43,14 @@ from .const import ( ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, 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, @@ -43,24 +61,30 @@ from .const import ( CONF_PARITY, CONF_PRECISION, CONF_REGISTER, + CONF_REVERSE_ORDER, CONF_SCALE, + CONF_SENSORS, CONF_STATE_CLOSED, CONF_STATE_CLOSING, + CONF_STATE_OFF, + CONF_STATE_ON, CONF_STATE_OPEN, CONF_STATE_OPENING, CONF_STATUS_REGISTER, CONF_STATUS_REGISTER_TYPE, CONF_STEP, CONF_STOPBITS, + CONF_SWITCHES, CONF_TARGET_TEMP, CONF_UNIT, + CONF_VERIFY_REGISTER, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, + DATA_TYPE_STRING, DATA_TYPE_UINT, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, - DEFAULT_SLAVE, DEFAULT_STRUCTURE_PREFIX, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, @@ -69,11 +93,40 @@ from .modbus import modbus_setup BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) -CLIMATE_SCHEMA = vol.Schema( + +def number(value: Any) -> Union[int, float]: + """Coerce a value to number without losing precision.""" + if isinstance(value, int): + return value + if isinstance(value, float): + return value + + 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 + + +BASE_COMPONENT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_SLAVE): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.positive_int, + } +) + + +CLIMATE_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { vol.Required(CONF_CURRENT_TEMP): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int, vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int, vol.Optional( @@ -84,9 +137,6 @@ CLIMATE_SCHEMA = vol.Schema( ), vol.Optional(CONF_PRECISION, default=1): cv.positive_int, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All( - cv.time_period, lambda value: value.total_seconds() - ), vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, @@ -98,14 +148,9 @@ CLIMATE_SCHEMA = vol.Schema( COVERS_SCHEMA = vol.All( cv.has_at_least_one_key(CALL_TYPE_COIL, CONF_REGISTER), - vol.Schema( + BASE_COMPONENT_SCHEMA.extend( { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All( - cv.time_period, lambda value: value.total_seconds() - ), vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_SLAVE, default=DEFAULT_SLAVE): cv.positive_int, vol.Optional(CONF_STATE_CLOSED, default=0): cv.positive_int, vol.Optional(CONF_STATE_CLOSING, default=3): cv.positive_int, vol.Optional(CONF_STATE_OPEN, default=1): cv.positive_int, @@ -121,33 +166,104 @@ COVERS_SCHEMA = vol.All( ), ) -SERIAL_SCHEMA = BASE_SCHEMA.extend( +SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_COIL] + ), + vol.Optional(CONF_COMMAND_OFF, default=0x00): cv.positive_int, + vol.Optional(CONF_COMMAND_ON, default=0x01): cv.positive_int, + vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int, + } +) + +SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_COUNT, default=1): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( + [ + DATA_TYPE_INT, + DATA_TYPE_UINT, + DATA_TYPE_FLOAT, + DATA_TYPE_STRING, + DATA_TYPE_CUSTOM, + ] + ), + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_OFFSET, default=0): number, + vol.Optional(CONF_PRECISION, default=0): cv.positive_int, + 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_SCALE, default=1): number, + vol.Optional(CONF_STRUCTURE): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +) + +BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In( + [CALL_TYPE_COIL, CALL_TYPE_DISCRETE] + ), + } +) + +MODBUS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string, + vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, + vol.Optional(CONF_DELAY, default=0): cv.positive_int, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSOR_SCHEMA] + ), + vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]), + vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), + } +) + +SERIAL_SCHEMA = MODBUS_SCHEMA.extend( + { + vol.Required(CONF_TYPE): "serial", vol.Required(CONF_BAUDRATE): cv.positive_int, vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8), vol.Required(CONF_METHOD): vol.Any("rtu", "ascii"), vol.Required(CONF_PORT): cv.string, vol.Required(CONF_PARITY): vol.Any("E", "O", "N"), vol.Required(CONF_STOPBITS): vol.Any(1, 2), - vol.Required(CONF_TYPE): "serial", - vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, - vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]), - vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), } ) -ETHERNET_SCHEMA = BASE_SCHEMA.extend( +ETHERNET_SCHEMA = MODBUS_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"), - vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, - vol.Optional(CONF_DELAY, default=0): cv.positive_int, - vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATE_SCHEMA]), - vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), } ) +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), + ], + ), + }, + extra=vol.ALLOW_EXTRA, +) + SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema( { vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, @@ -168,18 +284,6 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( } ) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), - ], - ), - }, - extra=vol.ALLOW_EXTRA, -) - def setup(hass, config): """Set up Modbus component.""" diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 8e91945d073..909f0088c38 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,5 +1,8 @@ """Support for Modbus Coil and Discrete Input sensors.""" -from typing import Optional +from __future__ import annotations + +from datetime import timedelta +import logging from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse @@ -10,19 +13,37 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, +) 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 .const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, + CONF_BINARY_SENSORS, CONF_COILS, CONF_HUB, CONF_INPUT_TYPE, CONF_INPUTS, DEFAULT_HUB, + DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN, ) +from .modbus import ModbusHub + +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_COILS, CONF_INPUTS), @@ -50,11 +71,33 @@ PLATFORM_SCHEMA = vol.All( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities, + discovery_info: DiscoveryInfoType | None = None, +): """Set up the Modbus binary sensors.""" sensors = [] - for entry in config[CONF_INPUTS]: - hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] + + #  check for old config: + if discovery_info is None: + _LOGGER.warning( + "Binary_sensor configuration is deprecated, will be removed in a future release" + ) + discovery_info = { + CONF_NAME: "no name", + CONF_BINARY_SENSORS: config[CONF_INPUTS], + } + config = None + + for entry in discovery_info[CONF_BINARY_SENSORS]: + if CONF_HUB in entry: + # from old config! + discovery_info[CONF_NAME] = entry[CONF_HUB] + 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, @@ -63,16 +106,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entry[CONF_ADDRESS], entry.get(CONF_DEVICE_CLASS), entry[CONF_INPUT_TYPE], + entry[CONF_SCAN_INTERVAL], ) ) - add_entities(sensors) + async_add_entities(sensors) class ModbusBinarySensor(BinarySensorEntity): """Modbus binary sensor.""" - def __init__(self, hub, name, slave, address, device_class, input_type): + def __init__( + self, hub, name, slave, address, device_class, input_type, scan_interval + ): """Initialize the Modbus binary sensor.""" self._hub = hub self._name = name @@ -82,6 +128,13 @@ class ModbusBinarySensor(BinarySensorEntity): self._input_type = input_type self._value = None self._available = True + self._scan_interval = timedelta(seconds=scan_interval) + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_track_time_interval( + self.hass, lambda arg: self._update(), self._scan_interval + ) @property def name(self): @@ -94,16 +147,26 @@ class ModbusBinarySensor(BinarySensorEntity): return self._value @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + + # Handle polling directly in this entity + return False + @property def available(self) -> bool: """Return True if entity is available.""" return self._available - def update(self): + def _update(self): """Update the state of the sensor.""" try: if self._input_type == CALL_TYPE_COIL: @@ -120,3 +183,4 @@ class ModbusBinarySensor(BinarySensorEntity): self._value = result.bits[0] & 1 self._available = True + self.schedule_update_ha_state() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 45cfbf5eb57..6ca1d5d63d3 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -1,8 +1,10 @@ """Support for Generic Modbus Thermostats.""" +from __future__ import annotations + from datetime import timedelta import logging import struct -from typing import Any, Dict, Optional +from typing import Any from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse @@ -57,7 +59,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, - discovery_info: Optional[DiscoveryInfoType] = None, + discovery_info: DiscoveryInfoType | None = None, ): """Read configuration and create Modbus climate.""" if discovery_info is None: @@ -108,12 +110,12 @@ class ModbusThermostat(ClimateEntity): def __init__( self, hub: ModbusHub, - config: Dict[str, Any], + config: dict[str, Any], ): """Initialize the modbus thermostat.""" self._hub: ModbusHub = hub self._name = config[CONF_NAME] - self._slave = config[CONF_SLAVE] + self._slave = config.get(CONF_SLAVE) self._target_temperature_register = config[CONF_TARGET_TEMP] self._current_temperature_register = config[CONF_CURRENT_TEMP] self._current_temperature_register_type = config[ @@ -232,7 +234,7 @@ class ModbusThermostat(ClimateEntity): self.schedule_update_ha_state() - def _read_register(self, register_type, register) -> Optional[float]: + def _read_register(self, register_type, register) -> float | None: """Read register using the Modbus hub slave.""" try: if register_type == CALL_TYPE_REGISTER_INPUT: @@ -274,7 +276,7 @@ class ModbusThermostat(ClimateEntity): def _write_register(self, register, value): """Write holding register using the Modbus hub slave.""" try: - self._hub.write_registers(self._slave, register, [value, 0]) + self._hub.write_registers(self._slave, register, value) except ConnectionException: self._available = False return diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index d3193cc004c..fde593aa966 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -50,6 +50,8 @@ 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" @@ -58,12 +60,16 @@ DEFAULT_STRUCT_FORMAT = { 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" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 09a465a2cdd..bc7c946402b 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -1,6 +1,8 @@ """Support for Modbus covers.""" +from __future__ import annotations + from datetime import timedelta -from typing import Any, Dict, Optional +from typing import Any from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse @@ -41,7 +43,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, - discovery_info: Optional[DiscoveryInfoType] = None, + discovery_info: DiscoveryInfoType | None = None, ): """Read configuration and create Modbus cover.""" if discovery_info is None: @@ -61,7 +63,7 @@ class ModbusCover(CoverEntity, RestoreEntity): def __init__( self, hub: ModbusHub, - config: Dict[str, Any], + config: dict[str, Any], ): """Initialize the modbus cover.""" self._hub: ModbusHub = hub @@ -69,7 +71,7 @@ class ModbusCover(CoverEntity, RestoreEntity): self._device_class = config.get(CONF_DEVICE_CLASS) self._name = config[CONF_NAME] self._register = config.get(CONF_REGISTER) - self._slave = config[CONF_SLAVE] + self._slave = config.get(CONF_SLAVE) self._state_closed = config[CONF_STATE_CLOSED] self._state_closing = config[CONF_STATE_CLOSING] self._state_open = config[CONF_STATE_OPEN] @@ -108,7 +110,7 @@ class ModbusCover(CoverEntity, RestoreEntity): ) @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class @@ -178,7 +180,7 @@ class ModbusCover(CoverEntity, RestoreEntity): self.schedule_update_ha_state() - def _read_status_register(self) -> Optional[int]: + 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: @@ -212,7 +214,7 @@ class ModbusCover(CoverEntity, RestoreEntity): self._available = True - def _read_coil(self) -> Optional[bool]: + def _read_coil(self) -> bool | None: """Read coil using the Modbus hub slave.""" try: result = self._hub.read_coils(self._slave, self._coil, 1) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 21c6caa6fcc..554b7bfb85e 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -25,12 +25,18 @@ from .const import ( 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, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, @@ -56,6 +62,9 @@ def modbus_setup( for component, conf_key in ( (CONF_CLIMATE, CONF_CLIMATES), (CONF_COVER, CONF_COVERS), + (CONF_BINARY_SENSOR, CONF_BINARY_SENSORS), + (CONF_SENSOR, CONF_SENSORS), + (CONF_SWITCH, CONF_SWITCHES), ): if conf_key in conf_hub: load_platform(hass, component, DOMAIN, conf_hub, config) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 656e5e2986d..7aa08070d67 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,23 +1,38 @@ """Support for Modbus Register sensors.""" +from __future__ import annotations + +from datetime import timedelta import logging import struct -from typing import Any, Optional, Union +from typing import Any from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol -from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( + CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, + CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_STRUCTURE, CONF_UNIT_OF_MEASUREMENT, ) 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 .const import ( CALL_TYPE_REGISTER_HOLDING, @@ -25,26 +40,30 @@ from .const import ( CONF_COUNT, CONF_DATA_TYPE, CONF_HUB, + CONF_INPUT_TYPE, CONF_PRECISION, CONF_REGISTER, CONF_REGISTER_TYPE, CONF_REGISTERS, CONF_REVERSE_ORDER, CONF_SCALE, + CONF_SENSORS, DATA_TYPE_CUSTOM, DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_STRING, DATA_TYPE_UINT, DEFAULT_HUB, + DEFAULT_SCAN_INTERVAL, DEFAULT_STRUCT_FORMAT, MODBUS_DOMAIN, ) +from .modbus import ModbusHub _LOGGER = logging.getLogger(__name__) -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 @@ -97,66 +116,92 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities, + discovery_info: DiscoveryInfoType | None = None, +): """Set up the Modbus sensors.""" sensors = [] - for register in config[CONF_REGISTERS]: - if register[CONF_DATA_TYPE] == DATA_TYPE_STRING: - structure = str(register[CONF_COUNT] * 2) + "s" - elif register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: + #  check for old config: + if discovery_info is None: + _LOGGER.warning( + "Sensor configuration is deprecated, will be removed in a future release" + ) + discovery_info = { + CONF_NAME: "no name", + CONF_SENSORS: config[CONF_REGISTERS], + } + for entry in discovery_info[CONF_SENSORS]: + entry[CONF_ADDRESS] = entry[CONF_REGISTER] + entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE] + del entry[CONF_REGISTER] + del entry[CONF_REGISTER_TYPE] + config = None + + for entry in discovery_info[CONF_SENSORS]: + if entry[CONF_DATA_TYPE] == DATA_TYPE_STRING: + structure = str(entry[CONF_COUNT] * 2) + "s" + elif entry[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: try: - structure = f">{DEFAULT_STRUCT_FORMAT[register[CONF_DATA_TYPE]][register[CONF_COUNT]]}" + structure = f">{DEFAULT_STRUCT_FORMAT[entry[CONF_DATA_TYPE]][entry[CONF_COUNT]]}" except KeyError: _LOGGER.error( "Unable to detect data type for %s sensor, try a custom type", - register[CONF_NAME], + entry[CONF_NAME], ) continue else: - structure = register.get(CONF_STRUCTURE) + structure = entry.get(CONF_STRUCTURE) try: size = struct.calcsize(structure) except struct.error as err: - _LOGGER.error("Error in sensor %s structure: %s", register[CONF_NAME], err) + _LOGGER.error("Error in sensor %s structure: %s", entry[CONF_NAME], err) continue - if register[CONF_COUNT] * 2 != size: + if entry[CONF_COUNT] * 2 != size: _LOGGER.error( "Structure size (%d bytes) mismatch registers count (%d words)", size, - register[CONF_COUNT], + entry[CONF_COUNT], ) continue - hub_name = register[CONF_HUB] - hub = hass.data[MODBUS_DOMAIN][hub_name] + if CONF_HUB in entry: + # from old config! + discovery_info[CONF_NAME] = entry[CONF_HUB] + 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, - register[CONF_NAME], - register.get(CONF_SLAVE), - register[CONF_REGISTER], - register[CONF_REGISTER_TYPE], - register.get(CONF_UNIT_OF_MEASUREMENT), - register[CONF_COUNT], - register[CONF_REVERSE_ORDER], - register[CONF_SCALE], - register[CONF_OFFSET], + 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], structure, - register[CONF_PRECISION], - register[CONF_DATA_TYPE], - register.get(CONF_DEVICE_CLASS), + entry[CONF_PRECISION], + entry[CONF_DATA_TYPE], + entry.get(CONF_DEVICE_CLASS), + entry[CONF_SCAN_INTERVAL], ) ) if not sensors: - return False - add_entities(sensors) + return + async_add_entities(sensors) -class ModbusRegisterSensor(RestoreEntity): +class ModbusRegisterSensor(RestoreEntity, SensorEntity): """Modbus register sensor.""" def __init__( @@ -175,6 +220,7 @@ class ModbusRegisterSensor(RestoreEntity): precision, data_type, device_class, + scan_interval, ): """Initialize the modbus register sensor.""" self._hub = hub @@ -193,13 +239,17 @@ class ModbusRegisterSensor(RestoreEntity): self._device_class = device_class self._value = None self._available = True + self._scan_interval = timedelta(seconds=scan_interval) async def async_added_to_hass(self): """Handle entity which will be added.""" state = await self.async_get_last_state() - if not state: - return - self._value = state.state + if state: + self._value = state.state + + async_track_time_interval( + self.hass, lambda arg: self._update(), self._scan_interval + ) @property def state(self): @@ -211,13 +261,23 @@ class ModbusRegisterSensor(RestoreEntity): """Return the name of the sensor.""" return self._name + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + + # Handle polling directly in this entity + return False + @property def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class @@ -226,7 +286,7 @@ class ModbusRegisterSensor(RestoreEntity): """Return True if entity is available.""" return self._available - def update(self): + def _update(self): """Update the state of the sensor.""" try: if self._register_type == CALL_TYPE_REGISTER_INPUT: @@ -279,3 +339,4 @@ class ModbusRegisterSensor(RestoreEntity): self._value = str(val) self._available = True + self.schedule_update_ha_state() diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 36fbef08428..2985d8b2c05 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,7 +1,10 @@ """Support for Modbus switches.""" -from abc import ABC +from __future__ import annotations + +from abc import ABC, abstractmethod +from datetime import timedelta import logging -from typing import Any, Dict, Optional +from typing import Any from pymodbus.exceptions import ConnectionException, ModbusException from pymodbus.pdu import ExceptionResponse @@ -9,14 +12,17 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( + CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_SLAVE, + CONF_SWITCHES, STATE_ON, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -26,6 +32,7 @@ from .const import ( CALL_TYPE_REGISTER_INPUT, CONF_COILS, CONF_HUB, + CONF_INPUT_TYPE, CONF_REGISTER, CONF_REGISTER_TYPE, CONF_REGISTERS, @@ -34,6 +41,7 @@ from .const import ( CONF_VERIFY_REGISTER, CONF_VERIFY_STATE, DEFAULT_HUB, + DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN, ) from .modbus import ModbusHub @@ -84,35 +92,72 @@ async def async_setup_platform( ): """Read configuration and create Modbus switches.""" switches = [] - if CONF_COILS in config: - for coil in config[CONF_COILS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][coil[CONF_HUB]] - switches.append(ModbusCoilSwitch(hub, coil)) - if CONF_REGISTERS in config: - for register in config[CONF_REGISTERS]: - hub: ModbusHub = hass.data[MODBUS_DOMAIN][register[CONF_HUB]] - switches.append(ModbusRegisterSwitch(hub, register)) + #  check for old config: + if discovery_info is None: + _LOGGER.warning( + "Switch configuration is deprecated, will be removed in a future release" + ) + discovery_info = { + CONF_NAME: "no name", + CONF_SWITCHES: [], + } + if CONF_COILS in config: + discovery_info[CONF_SWITCHES].extend(config[CONF_COILS]) + if CONF_REGISTERS in config: + discovery_info[CONF_SWITCHES].extend(config[CONF_REGISTERS]) + for entry in discovery_info[CONF_SWITCHES]: + if CALL_TYPE_COIL in entry: + entry[CONF_ADDRESS] = entry[CALL_TYPE_COIL] + entry[CONF_INPUT_TYPE] = CALL_TYPE_COIL + del entry[CALL_TYPE_COIL] + if CONF_REGISTER in entry: + entry[CONF_ADDRESS] = entry[CONF_REGISTER] + del entry[CONF_REGISTER] + if CONF_REGISTER_TYPE in entry: + entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE] + del entry[CONF_REGISTER_TYPE] + if CONF_SCAN_INTERVAL not in entry: + entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL + config = None + + 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]] + if entry[CONF_INPUT_TYPE] == CALL_TYPE_COIL: + switches.append(ModbusCoilSwitch(hub, entry)) + else: + switches.append(ModbusRegisterSwitch(hub, entry)) async_add_entities(switches) -class ModbusBaseSwitch(ToggleEntity, RestoreEntity, ABC): +class ModbusBaseSwitch(SwitchEntity, RestoreEntity, ABC): """Base class representing a Modbus switch.""" - def __init__(self, hub: ModbusHub, config: Dict[str, Any]): + def __init__(self, hub: ModbusHub, config: dict[str, Any]): """Initialize the switch.""" self._hub: ModbusHub = hub self._name = config[CONF_NAME] self._slave = config.get(CONF_SLAVE) self._is_on = None self._available = True + self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) async def async_added_to_hass(self): """Handle entity which will be added.""" state = await self.async_get_last_state() - if not state: - return - self._is_on = state.state == STATE_ON + if state: + self._is_on = state.state == STATE_ON + + async_track_time_interval( + self.hass, lambda arg: self._update(), self._scan_interval + ) + + @abstractmethod + def _update(self): + """Update the entity state.""" @property def is_on(self): @@ -124,6 +169,16 @@ class ModbusBaseSwitch(ToggleEntity, RestoreEntity, ABC): """Return the name of the switch.""" return self._name + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + + # Handle polling directly in this entity + return False + @property def available(self) -> bool: """Return True if entity is available.""" @@ -133,24 +188,27 @@ class ModbusBaseSwitch(ToggleEntity, RestoreEntity, ABC): class ModbusCoilSwitch(ModbusBaseSwitch, SwitchEntity): """Representation of a Modbus coil switch.""" - def __init__(self, hub: ModbusHub, config: Dict[str, Any]): + def __init__(self, hub: ModbusHub, config: dict[str, Any]): """Initialize the coil switch.""" super().__init__(hub, config) - self._coil = config[CALL_TYPE_COIL] + self._coil = config[CONF_ADDRESS] def turn_on(self, **kwargs): """Set switch on.""" self._write_coil(self._coil, True) self._is_on = True + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Set switch off.""" self._write_coil(self._coil, False) self._is_on = False + self.schedule_update_ha_state() - def update(self): + def _update(self): """Update the state of the switch.""" self._is_on = self._read_coil(self._coil) + self.schedule_update_ha_state() def _read_coil(self, coil) -> bool: """Read coil using the Modbus hub slave.""" @@ -184,17 +242,17 @@ class ModbusCoilSwitch(ModbusBaseSwitch, SwitchEntity): class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): """Representation of a Modbus register switch.""" - def __init__(self, hub: ModbusHub, config: Dict[str, Any]): + def __init__(self, hub: ModbusHub, config: dict[str, Any]): """Initialize the register switch.""" super().__init__(hub, config) - self._register = config[CONF_REGISTER] + self._register = config[CONF_ADDRESS] self._command_on = config[CONF_COMMAND_ON] self._command_off = config[CONF_COMMAND_OFF] self._state_on = config.get(CONF_STATE_ON, self._command_on) self._state_off = config.get(CONF_STATE_OFF, self._command_off) self._verify_state = config[CONF_VERIFY_STATE] self._verify_register = config.get(CONF_VERIFY_REGISTER, self._register) - self._register_type = config[CONF_REGISTER_TYPE] + self._register_type = config[CONF_INPUT_TYPE] self._available = True self._is_on = None @@ -205,6 +263,7 @@ class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): self._write_register(self._command_on) if not self._verify_state: self._is_on = True + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Set switch off.""" @@ -213,13 +272,14 @@ class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): self._write_register(self._command_off) if not self._verify_state: self._is_on = False + self.schedule_update_ha_state() @property def available(self) -> bool: """Return True if entity is available.""" return self._available - def update(self): + def _update(self): """Update the state of the switch.""" if not self._verify_state: return @@ -237,8 +297,9 @@ class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): self._register, value, ) + self.schedule_update_ha_state() - def _read_register(self) -> Optional[int]: + def _read_register(self) -> int | None: try: if self._register_type == CALL_TYPE_REGISTER_INPUT: result = self._hub.read_input_registers( diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index c58a4b67eed..080e077a457 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -4,7 +4,7 @@ import logging from basicmodem.basicmodem import BasicModem as bm import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -12,7 +12,6 @@ from homeassistant.const import ( STATE_IDLE, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Modem CallerID" @@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ModemCalleridSensor(hass, name, port, modem)]) -class ModemCalleridSensor(Entity): +class ModemCalleridSensor(SensorEntity): """Implementation of USB modem caller ID sensor.""" def __init__(self, hass, name, port, modem): @@ -86,7 +85,7 @@ class ModemCalleridSensor(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index e2d9909c7ca..7bfa161f9ec 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -5,7 +5,7 @@ import math import voluptuous as vol from homeassistant import util -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -17,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event _LOGGER = logging.getLogger(__name__) @@ -69,7 +68,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MoldIndicator(Entity): +class MoldIndicator(SensorEntity): """Represents a MoldIndication sensor.""" def __init__( @@ -375,7 +374,7 @@ class MoldIndicator(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._is_metric: return { diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 06883ddc8a8..adc0b05bab7 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -54,9 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): FIRST_RUN: first_run, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -67,8 +67,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 05158144916..a65fa8d23f3 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -16,8 +16,8 @@ from .const import ( CONF_SOURCE_5, CONF_SOURCE_6, CONF_SOURCES, + DOMAIN, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/monoprice/translations/hu.json b/homeassistant/components/monoprice/translations/hu.json index 892b8b2cd91..a845f862160 100644 --- a/homeassistant/components/monoprice/translations/hu.json +++ b/homeassistant/components/monoprice/translations/hu.json @@ -1,5 +1,12 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/monoprice/translations/id.json b/homeassistant/components/monoprice/translations/id.json new file mode 100644 index 00000000000..bf4269c492e --- /dev/null +++ b/homeassistant/components/monoprice/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "port": "Port", + "source_1": "Nama sumber #1", + "source_2": "Nama sumber #2", + "source_3": "Nama sumber #3", + "source_4": "Nama sumber #4", + "source_5": "Nama sumber #5", + "source_6": "Nama sumber #6" + }, + "title": "Hubungkan ke perangkat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nama sumber #1", + "source_2": "Nama sumber #2", + "source_3": "Nama sumber #3", + "source_4": "Nama sumber #4", + "source_5": "Nama sumber #5", + "source_6": "Nama sumber #6" + }, + "title": "Konfigurasikan sumber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/nl.json b/homeassistant/components/monoprice/translations/nl.json index 74bc677dbe8..28ecedab3d3 100644 --- a/homeassistant/components/monoprice/translations/nl.json +++ b/homeassistant/components/monoprice/translations/nl.json @@ -4,13 +4,13 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "port": "Seri\u00eble poort", + "port": "Poort", "source_1": "Naam van bron #1", "source_2": "Naam van bron #2", "source_3": "Naam van bron #3", diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 9e0f8ef51d6..4b373469cc6 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -2,10 +2,9 @@ from astral import Astral import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util DEFAULT_NAME = "Moon" @@ -42,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([MoonSensor(name)], True) -class MoonSensor(Entity): +class MoonSensor(SensorEntity): """Representation of a Moon sensor.""" def __init__(self, name): diff --git a/homeassistant/components/moon/translations/sensor.id.json b/homeassistant/components/moon/translations/sensor.id.json new file mode 100644 index 00000000000..197bc609813 --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.id.json @@ -0,0 +1,14 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "Seperempat pertama", + "full_moon": "Bulan purnama", + "last_quarter": "Seperempat ketiga", + "new_moon": "Bulan baru", + "waning_crescent": "Bulan sabit tua", + "waning_gibbous": "Bulan cembung tua", + "waxing_crescent": "Bulan sabit muda", + "waxing_gibbous": "Bulan cembung muda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 5d02d5a14a8..73a27c90140 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -5,6 +5,7 @@ import logging from socket import timeout from motionblinds import MotionMulticast +from motionblinds.motion_blinds import ParseException from homeassistant import config_entries, core from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP @@ -13,18 +14,87 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( + ATTR_AVAILABLE, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, MANUFACTURER, - MOTION_PLATFORMS, + PLATFORMS, + UPDATE_INTERVAL, + UPDATE_INTERVAL_FAST, ) from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) +class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): + """Class to manage fetching data from single endpoint.""" + + def __init__( + self, + hass, + logger, + gateway, + *, + name, + update_interval=None, + update_method=None, + ): + """Initialize global data updater.""" + super().__init__( + hass, + logger, + name=name, + update_method=update_method, + update_interval=update_interval, + ) + + self._gateway = gateway + + def update_gateway(self): + """Call all updates using one async_add_executor_job.""" + data = {} + + try: + self._gateway.Update() + except (timeout, ParseException): + # let the error be logged and handled by the motionblinds library + data[KEY_GATEWAY] = {ATTR_AVAILABLE: False} + return data + else: + data[KEY_GATEWAY] = {ATTR_AVAILABLE: True} + + for blind in self._gateway.device_list.values(): + try: + blind.Update() + except (timeout, ParseException): + # let the error be logged and handled by the motionblinds library + data[blind.mac] = {ATTR_AVAILABLE: False} + else: + data[blind.mac] = {ATTR_AVAILABLE: True} + + return data + + async def _async_update_data(self): + """Fetch the latest data from the gateway and blinds.""" + data = await self.hass.async_add_executor_job(self.update_gateway) + + all_available = True + for device in data.values(): + if not device[ATTR_AVAILABLE]: + all_available = False + break + + if all_available: + self.update_interval = timedelta(seconds=UPDATE_INTERVAL) + else: + self.update_interval = timedelta(seconds=UPDATE_INTERVAL_FAST) + + return data + + def setup(hass: core.HomeAssistant, config: dict): """Set up the Motion Blinds component.""" return True @@ -60,36 +130,18 @@ async def async_setup_entry( raise ConfigEntryNotReady motion_gateway = connect_gateway_class.gateway_device - def update_gateway(): - """Call all updates using one async_add_executor_job.""" - motion_gateway.Update() - for blind in motion_gateway.device_list.values(): - try: - blind.Update() - except timeout: - # let the error be logged and handled by the motionblinds library - pass - - async def async_update_data(): - """Fetch data from the gateway and blinds.""" - try: - await hass.async_add_executor_job(update_gateway) - except timeout: - # let the error be logged and handled by the motionblinds library - pass - - coordinator = DataUpdateCoordinator( + coordinator = DataUpdateCoordinatorMotionBlinds( hass, _LOGGER, + motion_gateway, # Name of the data. For logging purposes. name=entry.title, - update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=600), + update_interval=timedelta(seconds=UPDATE_INTERVAL), ) # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { KEY_GATEWAY: motion_gateway, @@ -107,9 +159,9 @@ async def async_setup_entry( sw_version=motion_gateway.protocol, ) - for component in MOTION_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -122,8 +174,8 @@ async def async_unload_entry( unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in MOTION_PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index d2bf216e26a..9aa62ca2d05 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST -# pylint: disable=unused-import from .const import DEFAULT_GATEWAY_NAME, DOMAIN from .gateway import ConnectMotionGateway diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index 27f2310c7ce..52c6e39b096 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -3,7 +3,7 @@ DOMAIN = "motion_blinds" MANUFACTURER = "Motion Blinds, Coulisse B.V." DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway" -MOTION_PLATFORMS = ["cover", "sensor"] +PLATFORMS = ["cover", "sensor"] KEY_GATEWAY = "gateway" KEY_COORDINATOR = "coordinator" @@ -11,5 +11,9 @@ KEY_MULTICAST_LISTENER = "multicast_listener" ATTR_WIDTH = "width" ATTR_ABSOLUTE_POSITION = "absolute_position" +ATTR_AVAILABLE = "available" SERVICE_SET_ABSOLUTE_POSITION = "set_absolute_position" + +UPDATE_INTERVAL = 600 +UPDATE_INTERVAL_FAST = 60 diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 3087401c3ae..2c4fee5f8aa 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -21,6 +21,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_ABSOLUTE_POSITION, + ATTR_AVAILABLE, ATTR_WIDTH, DOMAIN, KEY_COORDINATOR, @@ -160,7 +161,13 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): @property def available(self): """Return True if entity is available.""" - return self._blind.available + if self.coordinator.data is None: + return False + + if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]: + return False + + return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE] @property def current_cover_position(self): @@ -294,7 +301,7 @@ class MotionTDBUDevice(MotionPositionDevice): return self._blind.position[self._motor_key] == 100 @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" attributes = {} if self._blind.position is not None: diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index 14dd36ce5b0..6f8032e5a65 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -30,7 +30,7 @@ class ConnectMotionGateway: async def async_connect_gateway(self, host, key): """Connect to the Motion Gateway.""" - _LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3]) + _LOGGER.debug("Initializing with host %s (key %s)", host, key[:3]) self._gateway_device = MotionGateway( ip=host, key=key, multicast=self._multicast ) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index ec2823dbd2e..c144dc99bc5 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,6 +3,6 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.4.8"], + "requirements": ["motionblinds==0.4.10"], "codeowners": ["@starkillerOG"] } diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index f8a673b3079..d7f40337cec 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,16 +1,16 @@ """Support for Motion Blinds sensors.""" from motionblinds import BlindType +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_SIGNAL_STRENGTH, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY +from .const import ATTR_AVAILABLE, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY ATTR_BATTERY_VOLTAGE = "battery_voltage" TYPE_BLIND = "blind" @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class MotionBatterySensor(CoordinatorEntity, Entity): +class MotionBatterySensor(CoordinatorEntity, SensorEntity): """ Representation of a Motion Battery Sensor. @@ -70,7 +70,13 @@ class MotionBatterySensor(CoordinatorEntity, Entity): @property def available(self): """Return True if entity is available.""" - return self._blind.available + if self.coordinator.data is None: + return False + + if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]: + return False + + return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE] @property def unit_of_measurement(self): @@ -88,7 +94,7 @@ class MotionBatterySensor(CoordinatorEntity, Entity): return self._blind.battery_level @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} @@ -134,7 +140,7 @@ class MotionTDBUBatterySensor(MotionBatterySensor): return self._blind.battery_level[self._motor[0]] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" attributes = {} if self._blind.battery_voltage is not None: @@ -144,7 +150,7 @@ class MotionTDBUBatterySensor(MotionBatterySensor): return attributes -class MotionSignalStrengthSensor(CoordinatorEntity, Entity): +class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Representation of a Motion Signal Strength Sensor.""" def __init__(self, coordinator, device, device_type): @@ -174,7 +180,13 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity): @property def available(self): """Return True if entity is available.""" - return self._device.available + if self.coordinator.data is None: + return False + + if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]: + return False + + return self.coordinator.data[self._device.mac][ATTR_AVAILABLE] @property def unit_of_measurement(self): diff --git a/homeassistant/components/motion_blinds/translations/bg.json b/homeassistant/components/motion_blinds/translations/bg.json new file mode 100644 index 00000000000..39f706036fd --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "select": { + "data": { + "select_ip": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index c1a7ac0bc8d..01eba9c7ecd 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -10,12 +10,16 @@ "connect": { "data": { "api_key": "API-Schl\u00fcssel" - } + }, + "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Motion Jalousien" }, "select": { "data": { "select_ip": "IP-Adresse" - } + }, + "description": "F\u00fchre das Setup erneut aus, wenn du weitere Motion Gateways verbinden m\u00f6chtest", + "title": "W\u00e4hle das Motion Gateway aus, zu dem du eine Verbindung herstellen m\u00f6chten" }, "user": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json new file mode 100644 index 00000000000..541cefd2110 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "connection_error": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "connect": { + "data": { + "api_key": "API kulcs" + } + }, + "select": { + "data": { + "select_ip": "IP c\u00edm" + } + }, + "user": { + "data": { + "api_key": "API kulcs", + "host": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json new file mode 100644 index 00000000000..9248531a751 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/id.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "connection_error": "Gagal terhubung" + }, + "error": { + "discovery_error": "Gagal menemukan Motion Gateway" + }, + "flow_title": "Motion Blinds", + "step": { + "connect": { + "data": { + "api_key": "Kunci API" + }, + "description": "Anda akan memerlukan Kunci API 16 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Alamat IP" + }, + "description": "Jalankan penyiapan lagi jika ingin menghubungkan Motion Gateway lainnya", + "title": "Pilih Motion Gateway yang ingin dihubungkan" + }, + "user": { + "data": { + "api_key": "Kunci API", + "host": "Alamat IP" + }, + "description": "Hubungkan ke Motion Gateway Anda, jika alamat IP tidak disetel, penemuan otomatis akan digunakan", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ko.json b/homeassistant/components/motion_blinds/translations/ko.json index 9d2f0eead3d..69ed2cd7b35 100644 --- a/homeassistant/components/motion_blinds/translations/ko.json +++ b/homeassistant/components/motion_blinds/translations/ko.json @@ -5,22 +5,32 @@ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, + "error": { + "discovery_error": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucc3e\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Motion Blinds", "step": { "connect": { "data": { "api_key": "API \ud0a4" - } + }, + "description": "16\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API Key\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "Motion Blinds" }, "select": { "data": { "select_ip": "IP \uc8fc\uc18c" - } + }, + "description": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucd94\uac00 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", + "title": "\uc5f0\uacb0\ud560 Motion \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30" }, "user": { "data": { "api_key": "API \ud0a4", "host": "IP \uc8fc\uc18c" - } + }, + "description": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", + "title": "Motion Blinds" } } } diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json index 01cb117bb5b..54baeb9e18d 100644 --- a/homeassistant/components/motion_blinds/translations/nl.json +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -8,24 +8,29 @@ "error": { "discovery_error": "Kan geen Motion Gateway vinden" }, + "flow_title": "Motion Blinds", "step": { "connect": { "data": { "api_key": "API-sleutel" }, + "description": "U hebt de API-sleutel van 16 tekens nodig, zie https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key voor instructies", "title": "Motion Blinds" }, "select": { "data": { "select_ip": "IP-adres" }, + "description": "Voer de installatie opnieuw uit als u extra Motion Gateways wilt aansluiten", "title": "Selecteer de Motion Gateway waarmee u verbinding wilt maken" }, "user": { "data": { "api_key": "API-sleutel", "host": "IP-adres" - } + }, + "description": "Maak verbinding met uw Motion Gateway, als het IP-adres niet is ingesteld, wordt auto-discovery gebruikt", + "title": "Motion Blinds" } } } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 371d2060680..adb4bf0e810 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -1,4 +1,5 @@ """Support to interact with a Music Player Daemon.""" +from contextlib import suppress from datetime import timedelta import hashlib import logging @@ -129,10 +130,8 @@ class MpdDevice(MediaPlayerEntity): def _disconnect(self): """Disconnect from MPD.""" - try: + with suppress(mpd.ConnectionError): self._client.disconnect() - except mpd.ConnectionError: - pass self._is_connected = False self._status = None diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 29f7e24da00..ce2d413e1b6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,4 +1,6 @@ """Support for MQTT message handling.""" +from __future__ import annotations + import asyncio from functools import lru_cache, partial, wraps import inspect @@ -8,7 +10,7 @@ from operator import attrgetter import os import ssl import time -from typing import Any, Callable, List, Optional, Union +from typing import Any, Callable, Union import uuid import attr @@ -310,7 +312,7 @@ async def async_subscribe( topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, - encoding: Optional[str] = "utf-8", + encoding: str | None = "utf-8", ): """Subscribe to an MQTT topic. @@ -385,7 +387,7 @@ async def _async_setup_discovery( async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Start the MQTT protocol service.""" - conf: Optional[ConfigType] = config.get(DOMAIN) + conf: ConfigType | None = config.get(DOMAIN) websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_remove_device) @@ -552,7 +554,7 @@ class MQTT: self.hass = hass self.config_entry = config_entry self.conf = conf - self.subscriptions: List[Subscription] = [] + self.subscriptions: list[Subscription] = [] self.connected = False self._ha_started = asyncio.Event() self._last_subscribe = time.time() @@ -668,7 +670,7 @@ class MQTT: will_message = None if will_message is not None: - self._mqttc.will_set( # pylint: disable=no-value-for-parameter + self._mqttc.will_set( topic=will_message.topic, payload=will_message.payload, qos=will_message.qos, @@ -730,7 +732,7 @@ class MQTT: topic: str, msg_callback: MessageCallbackType, qos: int, - encoding: Optional[str] = None, + encoding: str | None = None, ) -> Callable[[], None]: """Set up a subscription to a topic with the provided qos. @@ -833,7 +835,7 @@ class MQTT: async def publish_birth_message(birth_message): await self._ha_started.wait() # Wait for Home Assistant to start await self._discovery_cooldown() # Wait for MQTT discovery to cool down - await self.async_publish( # pylint: disable=no-value-for-parameter + await self.async_publish( topic=birth_message.topic, payload=birth_message.payload, qos=birth_message.qos, diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 8868d487f93..78d0434e412 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -24,6 +24,7 @@ ABBREVIATIONS = { "bat_lev_tpl": "battery_level_template", "chrg_t": "charging_topic", "chrg_tpl": "charging_template", + "clrm": "color_mode", "clr_temp_cmd_t": "color_temp_command_topic", "clr_temp_stat_t": "color_temp_state_topic", "clr_temp_tpl": "color_temp_template", @@ -42,6 +43,7 @@ ABBREVIATIONS = { "dev_cla": "device_class", "dock_t": "docked_topic", "dock_tpl": "docked_template", + "en": "enabled_by_default", "err_t": "error_topic", "err_tpl": "error_template", "fanspd_t": "fan_speed_topic", @@ -86,8 +88,13 @@ ABBREVIATIONS = { "on_cmd_type": "on_command_type", "opt": "optimistic", "osc_cmd_t": "oscillation_command_topic", + "osc_cmd_tpl": "oscillation_command_template", "osc_stat_t": "oscillation_state_topic", "osc_val_tpl": "oscillation_value_template", + "pct_cmd_t": "percentage_command_topic", + "pct_cmd_tpl": "percentage_command_template", + "pct_stat_t": "percentage_state_topic", + "pct_val_tpl": "percentage_value_template", "pl": "payload", "pl_arm_away": "payload_arm_away", "pl_arm_home": "payload_arm_home", @@ -124,6 +131,11 @@ ABBREVIATIONS = { "pow_cmd_t": "power_command_topic", "pow_stat_t": "power_state_topic", "pow_stat_tpl": "power_state_template", + "pr_mode_cmd_t": "preset_mode_command_topic", + "pr_mode_cmd_tpl": "preset_mode_command_template", + "pr_mode_stat_t": "preset_mode_state_topic", + "pr_mode_val_tpl": "preset_mode_value_template", + "pr_modes": "preset_modes", "r_tpl": "red_template", "ret": "retain", "rgb_cmd_tpl": "rgb_command_template", @@ -139,6 +151,8 @@ ABBREVIATIONS = { "pos_tpl": "position_template", "spd_cmd_t": "speed_command_topic", "spd_stat_t": "speed_state_topic", + "spd_rng_min": "speed_range_min", + "spd_rng_max": "speed_range_max", "spd_val_tpl": "speed_value_template", "spds": "speeds", "src_type": "source_type", @@ -156,6 +170,7 @@ ABBREVIATIONS = { "stat_val_tpl": "state_value_template", "stype": "subtype", "sup_feat": "supported_features", + "sup_clrm": "supported_color_modes", "swing_mode_cmd_tpl": "swing_mode_command_template", "swing_mode_cmd_t": "swing_mode_command_topic", "swing_mode_stat_tpl": "swing_mode_state_template", @@ -203,4 +218,5 @@ DEVICE_ABBREVIATIONS = { "mf": "manufacturer", "mdl": "model", "sw": "sw_version", + "sa": "suggested_area", } diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 2f2ba06d6d7..0f10e91e41c 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -14,9 +14,7 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.const import ( CONF_CODE, - CONF_DEVICE, CONF_NAME, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -44,13 +42,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper _LOGGER = logging.getLogger(__name__) @@ -70,34 +62,28 @@ DEFAULT_ARM_HOME = "ARM_HOME" DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" DEFAULT_DISARM = "DISARM" DEFAULT_NAME = "MQTT Alarm" -PLATFORM_SCHEMA = ( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CODE): cv.string, - vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, - vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, - vol.Optional( - CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE - ): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, - vol.Optional( - CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS - ): cv.string, - vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, + vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, + vol.Optional( + CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE + ): cv.template, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, + vol.Optional( + CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS + ): cv.string, + vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -138,7 +124,6 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): return PLATFORM_SCHEMA def _setup_from_config(self, config): - self._config = config value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass @@ -186,11 +171,6 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): }, ) - @property - def name(self): - """Return the name of the device.""" - return self._config[CONF_NAME] - @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index b9fb297cd5c..fbd5e7535c5 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -11,13 +11,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.const import ( - CONF_DEVICE, CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback @@ -32,9 +30,7 @@ from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription from .. import mqtt from .debug_info import log_messages from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, + MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, async_setup_entry_helper, @@ -49,23 +45,17 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -PLATFORM_SCHEMA = ( - mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OFF_DELAY): cv.positive_int, - vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, - vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OFF_DELAY): cv.positive_int, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -113,7 +103,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): return PLATFORM_SCHEMA def _setup_from_config(self, config): - self._config = config value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass @@ -218,11 +207,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): self.async_write_ha_state() - @property - def name(self): - """Return the name of the binary sensor.""" - return self._config[CONF_NAME] - @property def is_on(self): """Return true if the binary sensor is on.""" @@ -242,7 +226,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): def available(self) -> bool: """Return true if the device is available and value has not expired.""" expire_after = self._config.get(CONF_EXPIRE_AFTER) - # pylint: disable=no-member return MqttAvailability.available.fget(self) and ( expire_after is None or not self._expired ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index cc58741a923..0a1a35b2ddd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components import camera from homeassistant.components.camera import Camera -from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service @@ -14,29 +14,17 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import CONF_QOS, DOMAIN, PLATFORMS, subscription from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper CONF_TOPIC = "topic" DEFAULT_NAME = "MQTT Camera" -PLATFORM_SCHEMA = ( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -77,9 +65,6 @@ class MqttCamera(MqttEntity, Camera): """Return the config schema.""" return PLATFORM_SCHEMA - def _setup_from_config(self, config): - self._config = config - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -105,8 +90,3 @@ class MqttCamera(MqttEntity, Camera): async def async_camera_image(self): """Return image response.""" return self._last_image - - @property - def name(self): - """Return the name of this camera.""" - return self._config[CONF_NAME] diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index ede39103791..8ab7a9ca3cf 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -36,12 +36,10 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, - CONF_DEVICE, CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_TEMPERATURE_UNIT, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, PRECISION_HALVES, PRECISION_TENTHS, @@ -63,13 +61,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper _LOGGER = logging.getLogger(__name__) @@ -178,90 +170,84 @@ TOPIC_KEYS = ( ) SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) -PLATFORM_SCHEMA = ( - SCHEMA_BASE.extend( - { - vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, - vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_FAN_MODE_LIST, - default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], - ): cv.ensure_list, - vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list, - vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_MODE_LIST, - default=[ - HVAC_MODE_AUTO, - HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - ], - ): cv.ensure_list, - vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, - vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, - vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_PRECISION): vol.In( - [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] - ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, - vol.Optional(CONF_ACTION_TEMPLATE): cv.template, - vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF] - ): cv.ensure_list, - vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int, - vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), - vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = SCHEMA_BASE.extend( + { + vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional( + CONF_FAN_MODE_LIST, + default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], + ): cv.ensure_list, + vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list, + vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional( + CONF_MODE_LIST, + default=[ + HVAC_MODE_AUTO, + HVAC_MODE_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + ], + ): cv.ensure_list, + vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, + vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PRECISION): vol.In( + [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + ), + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, + vol.Optional(CONF_ACTION_TEMPLATE): cv.template, + vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional( + CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF] + ): cv.ensure_list, + vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int, + vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), + vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -321,7 +307,6 @@ class MqttClimate(MqttEntity, ClimateEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config self._topic = {key: config.get(key) for key in TOPIC_KEYS} # set to None in non-optimistic mode @@ -563,11 +548,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self.hass, self._sub_state, topics ) - @property - def name(self): - """Return the name of the climate device.""" - return self._config[CONF_NAME] - @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index e19aaecc3db..5e5b8c54cf2 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -27,11 +27,12 @@ from .const import ( DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, + DOMAIN, ) from .util import MQTT_WILL_BIRTH_SCHEMA -@config_entries.HANDLERS.register("mqtt") +@config_entries.HANDLERS.register(DOMAIN) class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index e2a16c25329..010f751dad4 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -20,11 +20,9 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.const import ( - CONF_DEVICE, CONF_DEVICE_CLASS, CONF_NAME, CONF_OPTIMISTIC, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_CLOSED, STATE_CLOSING, @@ -48,13 +46,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper _LOGGER = logging.getLogger(__name__) @@ -128,7 +120,7 @@ def validate_options(value): and CONF_VALUE_TEMPLATE in value ): _LOGGER.warning( - "using 'value_template' for 'position_topic' is deprecated " + "Using 'value_template' for 'position_topic' is deprecated " "and will be removed from Home Assistant in version 2021.6, " "please replace it with 'position_template'" ) @@ -147,7 +139,6 @@ PLATFORM_SCHEMA = vol.All( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -183,14 +174,11 @@ PLATFORM_SCHEMA = vol.All( ): cv.boolean, vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template, } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema), + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), validate_options, ) @@ -238,7 +226,6 @@ class MqttCover(MqttEntity, CoverEntity): return PLATFORM_SCHEMA def _setup_from_config(self, config): - self._config = config self._optimistic = config[CONF_OPTIMISTIC] or ( config.get(CONF_STATE_TOPIC) is None and config.get(CONF_GET_POSITION_TOPIC) is None @@ -410,11 +397,6 @@ class MqttCover(MqttEntity, CoverEntity): """Return true if we do optimistic updates.""" return self._optimistic - @property - def name(self): - """Return the name of the cover.""" - return self._config[CONF_NAME] - @property def is_closed(self): """Return true if the cover is closed or None if the status is unknown.""" diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index bd5d9a1e60e..d6688636bb2 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -10,10 +10,7 @@ from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_DEVICE, - CONF_ICON, CONF_NAME, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_HOME, STATE_NOT_HOME, @@ -25,33 +22,20 @@ from .. import subscription from ... import mqtt from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_DISCOVERY = ( - mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, - vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, - vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA_DISCOVERY = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): @@ -87,8 +71,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config - value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass @@ -125,39 +107,34 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): }, ) - @property - def icon(self): - """Return the icon of the device.""" - return self._config.get(CONF_ICON) - @property def latitude(self): - """Return latitude if provided in device_state_attributes or None.""" + """Return latitude if provided in extra_state_attributes or None.""" if ( - self.device_state_attributes is not None - and ATTR_LATITUDE in self.device_state_attributes + self.extra_state_attributes is not None + and ATTR_LATITUDE in self.extra_state_attributes ): - return self.device_state_attributes[ATTR_LATITUDE] + return self.extra_state_attributes[ATTR_LATITUDE] return None @property def location_accuracy(self): - """Return location accuracy if provided in device_state_attributes or None.""" + """Return location accuracy if provided in extra_state_attributes or None.""" if ( - self.device_state_attributes is not None - and ATTR_GPS_ACCURACY in self.device_state_attributes + self.extra_state_attributes is not None + and ATTR_GPS_ACCURACY in self.extra_state_attributes ): - return self.device_state_attributes[ATTR_GPS_ACCURACY] + return self.extra_state_attributes[ATTR_GPS_ACCURACY] return None @property def longitude(self): - """Return longitude if provided in device_state_attributes or None.""" + """Return longitude if provided in extra_state_attributes or None.""" if ( - self.device_state_attributes is not None - and ATTR_LONGITUDE in self.device_state_attributes + self.extra_state_attributes is not None + and ATTR_LONGITUDE in self.extra_state_attributes ): - return self.device_state_attributes[ATTR_LONGITUDE] + return self.extra_state_attributes[ATTR_LONGITUDE] return None @property @@ -165,11 +142,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): """Return a location name for the current location of the device.""" return self._location_name - @property - def name(self): - """Return the name of the device tracker.""" - return self._config.get(CONF_NAME) - @property def source_type(self): """Return the source type, eg gps or router, of the device.""" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index d6e2ee0fc65..1e058162bc3 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -1,6 +1,8 @@ """Provides device automations for MQTT.""" +from __future__ import annotations + import logging -from typing import Callable, List, Optional +from typing import Callable import attr import voluptuous as vol @@ -85,8 +87,8 @@ class TriggerInstance: action: AutomationActionType = attr.ib() automation_info: dict = attr.ib() - trigger: "Trigger" = attr.ib() - remove: Optional[CALLBACK_TYPE] = attr.ib(default=None) + trigger: Trigger = attr.ib() + remove: CALLBACK_TYPE | None = attr.ib(default=None) async def async_attach_trigger(self): """Attach MQTT trigger.""" @@ -126,7 +128,7 @@ class Trigger: topic: str = attr.ib() type: str = attr.ib() value_template: str = attr.ib() - trigger_instances: List[TriggerInstance] = attr.ib(factory=list) + trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger(self, action, automation_info): """Add MQTT trigger.""" @@ -285,7 +287,7 @@ async def async_device_removed(hass: HomeAssistant, device_id: str): ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for MQTT devices.""" triggers = [] @@ -317,7 +319,6 @@ async def async_attach_trigger( """Attach a trigger.""" if DEVICE_TRIGGERS not in hass.data: hass.data[DEVICE_TRIGGERS] = {} - config = TRIGGER_SCHEMA(config) device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 24d652062ae..395480a041d 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,32 +1,42 @@ """Support for MQTT fans.""" import functools +import logging import voluptuous as vol from homeassistant.components import fan from homeassistant.components.fan import ( + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, ATTR_SPEED, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_OSCILLATE, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, + speed_list_without_preset_modes, ) from homeassistant.const import ( - CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_STATE, - CONF_UNIQUE_ID, ) from homeassistant.core import 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.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import ( CONF_COMMAND_TOPIC, @@ -39,21 +49,28 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper CONF_STATE_VALUE_TEMPLATE = "state_value_template" +CONF_COMMAND_TEMPLATE = "command_template" +CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" +CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" +CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template" +CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" +CONF_SPEED_RANGE_MIN = "speed_range_min" +CONF_SPEED_RANGE_MAX = "speed_range_max" +CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" +CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" +CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" +CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" +CONF_PRESET_MODES_LIST = "preset_modes" CONF_SPEED_STATE_TOPIC = "speed_state_topic" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_VALUE_TEMPLATE = "speed_value_template" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" +CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" CONF_PAYLOAD_OFF_SPEED = "payload_off_speed" @@ -66,21 +83,74 @@ DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_OPTIMISTIC = False +DEFAULT_SPEED_RANGE_MIN = 1 +DEFAULT_SPEED_RANGE_MAX = 100 OSCILLATE_ON_PAYLOAD = "oscillate_on" OSCILLATE_OFF_PAYLOAD = "oscillate_off" -OSCILLATION = "oscillation" -PLATFORM_SCHEMA = ( +_LOGGER = logging.getLogger(__name__) + + +def valid_fan_speed_configuration(config): + """Validate that the fan speed configuration is valid, throws if it isn't.""" + if config.get(CONF_SPEED_COMMAND_TOPIC) and not speed_list_without_preset_modes( + config.get(CONF_SPEED_LIST) + ): + raise ValueError("No valid speeds configured") + return config + + +def valid_speed_range_configuration(config): + """Validate that the fan speed_range configuration is valid, throws if it isn't.""" + if config.get(CONF_SPEED_RANGE_MIN) == 0: + raise ValueError("speed_range_min must be > 0") + if config.get(CONF_SPEED_RANGE_MIN) >= config.get(CONF_SPEED_RANGE_MAX): + raise ValueError("speed_range_max must be > speed_range_min") + return config + + +PLATFORM_SCHEMA = vol.All( + # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and + # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, + # are deprecated, support will be removed after a quarter (2021.7) + cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), + cv.deprecated(CONF_PAYLOAD_LOW_SPEED), + cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), + cv.deprecated(CONF_SPEED_LIST), + cv.deprecated(CONF_SPEED_COMMAND_TOPIC), + cv.deprecated(CONF_SPEED_STATE_TOPIC), + cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template, + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + vol.Inclusive( + CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" + ): mqtt.valid_publish_topic, + vol.Inclusive( + CONF_PRESET_MODES_LIST, "preset_modes", default=[] + ): cv.ensure_list, + vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, + vol.Optional( + CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN + ): cv.positive_int, + vol.Optional( + CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX + ): cv.positive_int, vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, @@ -101,11 +171,10 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + valid_fan_speed_configuration, + valid_speed_range_configuration, ) @@ -138,15 +207,21 @@ class MqttFan(MqttEntity, FanEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" self._state = False + # self._speed will be removed after a quarter (2021.7) self._speed = None + self._percentage = None + self._preset_mode = None self._oscillation = None self._supported_features = 0 self._topic = None self._payload = None - self._templates = None + self._value_templates = None + self._command_templates = None self._optimistic = None self._optimistic_oscillation = None + self._optimistic_percentage = None + self._optimistic_preset_mode = None self._optimistic_speed = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -158,38 +233,91 @@ class MqttFan(MqttEntity, FanEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config + self._speed_range = ( + config.get(CONF_SPEED_RANGE_MIN), + config.get(CONF_SPEED_RANGE_MAX), + ) self._topic = { key: config.get(key) for key in ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + CONF_PERCENTAGE_STATE_TOPIC, + CONF_PERCENTAGE_COMMAND_TOPIC, + CONF_PRESET_MODE_STATE_TOPIC, + CONF_PRESET_MODE_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_SPEED_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_OSCILLATION_COMMAND_TOPIC, ) } - self._templates = { + self._value_templates = { CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE), + ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE), + # ATTR_SPEED is deprecated in the schema, support will be removed after a quarter (2021.7) ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE), - OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), + ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), + } + self._command_templates = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE), + ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE), + ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE), } self._payload = { "STATE_ON": config[CONF_PAYLOAD_ON], "STATE_OFF": config[CONF_PAYLOAD_OFF], "OSCILLATE_ON_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_ON], "OSCILLATE_OFF_PAYLOAD": config[CONF_PAYLOAD_OSCILLATION_OFF], + # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) "SPEED_LOW": config[CONF_PAYLOAD_LOW_SPEED], "SPEED_MEDIUM": config[CONF_PAYLOAD_MEDIUM_SPEED], "SPEED_HIGH": config[CONF_PAYLOAD_HIGH_SPEED], "SPEED_OFF": config[CONF_PAYLOAD_OFF_SPEED], } + # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) + self._feature_legacy_speeds = not self._topic[CONF_SPEED_COMMAND_TOPIC] is None + if self._feature_legacy_speeds: + self._legacy_speeds_list = config[CONF_SPEED_LIST] + self._legacy_speeds_list_no_off = speed_list_without_preset_modes( + self._legacy_speeds_list + ) + else: + self._legacy_speeds_list = [] + + self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config + self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config + if self._feature_preset_mode: + self._speeds_list = speed_list_without_preset_modes( + self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST] + ) + self._preset_modes = ( + self._legacy_speeds_list + config[CONF_PRESET_MODES_LIST] + ) + else: + self._speeds_list = speed_list_without_preset_modes( + self._legacy_speeds_list + ) + self._preset_modes = [] + + if not self._speeds_list or self._feature_percentage: + self._speed_count = 100 + else: + self._speed_count = len(self._speeds_list) + optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None self._optimistic_oscillation = ( optimistic or self._topic[CONF_OSCILLATION_STATE_TOPIC] is None ) + self._optimistic_percentage = ( + optimistic or self._topic[CONF_PERCENTAGE_STATE_TOPIC] is None + ) + self._optimistic_preset_mode = ( + optimistic or self._topic[CONF_PRESET_MODE_STATE_TOPIC] is None + ) self._optimistic_speed = ( optimistic or self._topic[CONF_SPEED_STATE_TOPIC] is None ) @@ -199,16 +327,22 @@ class MqttFan(MqttEntity, FanEntity): self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None and SUPPORT_OSCILLATE ) - self._supported_features |= ( - self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED - ) + if self._feature_preset_mode and self._speeds_list: + self._supported_features |= SUPPORT_SET_SPEED + if self._feature_percentage: + self._supported_features |= SUPPORT_SET_SPEED + if self._feature_legacy_speeds: + self._supported_features |= SUPPORT_SET_SPEED + if self._feature_preset_mode: + self._supported_features |= SUPPORT_PRESET_MODE - for key, tpl in list(self._templates.items()): - if tpl is None: - self._templates[key] = lambda value: value - else: - tpl.hass = self.hass - self._templates[key] = tpl.async_render_with_possible_json_value + for tpl_dict in [self._command_templates, self._value_templates]: + for key, tpl in tpl_dict.items(): + if tpl is None: + tpl_dict[key] = lambda value: value + else: + tpl.hass = self.hass + tpl_dict[key] = tpl.async_render_with_possible_json_value async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -218,7 +352,7 @@ class MqttFan(MqttEntity, FanEntity): @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new received MQTT message.""" - payload = self._templates[CONF_STATE](msg.payload) + payload = self._value_templates[CONF_STATE](msg.payload) if payload == self._payload["STATE_ON"]: self._state = True elif payload == self._payload["STATE_OFF"]: @@ -232,19 +366,103 @@ class MqttFan(MqttEntity, FanEntity): "qos": self._config[CONF_QOS], } + @callback + @log_messages(self.hass, self.entity_id) + def percentage_received(msg): + """Handle new received MQTT message for the percentage.""" + numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload) + try: + percentage = ranged_value_to_percentage( + self._speed_range, int(numeric_val_str) + ) + except ValueError: + _LOGGER.warning( + "'%s' received on topic %s is not a valid speed within the speed range", + msg.payload, + msg.topic, + ) + return + if percentage < 0 or percentage > 100: + _LOGGER.warning( + "'%s' received on topic %s is not a valid speed within the speed range", + msg.payload, + msg.topic, + ) + return + self._percentage = percentage + self.async_write_ha_state() + + if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: + topics[CONF_PERCENTAGE_STATE_TOPIC] = { + "topic": self._topic[CONF_PERCENTAGE_STATE_TOPIC], + "msg_callback": percentage_received, + "qos": self._config[CONF_QOS], + } + self._percentage = None + + @callback + @log_messages(self.hass, self.entity_id) + def preset_mode_received(msg): + """Handle new received MQTT message for preset mode.""" + preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + if preset_mode not in self.preset_modes: + _LOGGER.warning( + "'%s' received on topic %s is not a valid preset mode", + msg.payload, + msg.topic, + ) + return + + self._preset_mode = preset_mode + if not self._implemented_percentage and (preset_mode in self.speed_list): + self._percentage = ordered_list_item_to_percentage( + self.speed_list, preset_mode + ) + self.async_write_ha_state() + + if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: + topics[CONF_PRESET_MODE_STATE_TOPIC] = { + "topic": self._topic[CONF_PRESET_MODE_STATE_TOPIC], + "msg_callback": preset_mode_received, + "qos": self._config[CONF_QOS], + } + self._preset_mode = None + + # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) @callback @log_messages(self.hass, self.entity_id) def speed_received(msg): """Handle new received MQTT message for the speed.""" - payload = self._templates[ATTR_SPEED](msg.payload) - if payload == self._payload["SPEED_LOW"]: - self._speed = SPEED_LOW - elif payload == self._payload["SPEED_MEDIUM"]: - self._speed = SPEED_MEDIUM - elif payload == self._payload["SPEED_HIGH"]: - self._speed = SPEED_HIGH - elif payload == self._payload["SPEED_OFF"]: - self._speed = SPEED_OFF + speed_payload = self._value_templates[ATTR_SPEED](msg.payload) + if speed_payload == self._payload["SPEED_LOW"]: + speed = SPEED_LOW + elif speed_payload == self._payload["SPEED_MEDIUM"]: + speed = SPEED_MEDIUM + elif speed_payload == self._payload["SPEED_HIGH"]: + speed = SPEED_HIGH + elif speed_payload == self._payload["SPEED_OFF"]: + speed = SPEED_OFF + else: + speed = None + + if speed and speed in self._legacy_speeds_list: + self._speed = speed + else: + _LOGGER.warning( + "'%s' received on topic %s is not a valid speed", + msg.payload, + msg.topic, + ) + return + + if not self._implemented_percentage: + if speed in self._speeds_list: + self._percentage = ordered_list_item_to_percentage( + self._speeds_list, speed + ) + elif speed == SPEED_OFF: + self._percentage = 0 + self.async_write_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: @@ -259,7 +477,7 @@ class MqttFan(MqttEntity, FanEntity): @log_messages(self.hass, self.entity_id) def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" - payload = self._templates[OSCILLATION](msg.payload) + payload = self._value_templates[ATTR_OSCILLATING](msg.payload) if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: @@ -289,14 +507,41 @@ class MqttFan(MqttEntity, FanEntity): return self._state @property - def name(self) -> str: - """Get entity name.""" - return self._config[CONF_NAME] + def _implemented_percentage(self): + """Return true if percentage has been implemented.""" + return self._feature_percentage + @property + def _implemented_preset_mode(self): + """Return true if preset_mode has been implemented.""" + return self._feature_preset_mode + + # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) + @property + def _implemented_speed(self): + """Return true if speed has been implemented.""" + return self._feature_legacy_speeds + + @property + def percentage(self): + """Return the current percentage.""" + return self._percentage + + @property + def preset_mode(self): + """Return the current preset _mode.""" + return self._preset_mode + + @property + def preset_modes(self) -> list: + """Get the list of available preset modes.""" + return self._preset_modes + + # The speed_list property is deprecated in the schema, support will be removed after a quarter (2021.7) @property def speed_list(self) -> list: """Get the list of available speeds.""" - return self._config[CONF_SPEED_LIST] + return self._speeds_list @property def supported_features(self) -> int: @@ -308,18 +553,17 @@ class MqttFan(MqttEntity, FanEntity): """Return the current speed.""" return self._speed + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports or 100 if percentage is supported.""" + return self._speed_count + @property def oscillating(self): """Return the oscillation state.""" return self._oscillation - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # + # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) async def async_turn_on( self, speed: str = None, @@ -331,14 +575,20 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ + mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_ON"]) mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], - self._payload["STATE_ON"], + mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) - if speed: + if percentage: + await self.async_set_percentage(percentage) + if preset_mode: + await self.async_set_preset_mode(preset_mode) + # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) + if speed and not percentage and not preset_mode: await self.async_set_speed(speed) if self._optimistic: self._state = True @@ -349,10 +599,11 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ + mqtt_payload = self._command_templates[CONF_STATE](self._payload["STATE_OFF"]) mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], - self._payload["STATE_OFF"], + mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) @@ -360,32 +611,112 @@ class MqttFan(MqttEntity, FanEntity): self._state = False self.async_write_ha_state() - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan. + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan. This method is a coroutine. """ - if speed == SPEED_LOW: - mqtt_payload = self._payload["SPEED_LOW"] - elif speed == SPEED_MEDIUM: - mqtt_payload = self._payload["SPEED_MEDIUM"] - elif speed == SPEED_HIGH: - mqtt_payload = self._payload["SPEED_HIGH"] - elif speed == SPEED_OFF: - mqtt_payload = self._payload["SPEED_OFF"] - else: - raise ValueError(f"{speed} is not a valid fan speed") + percentage_payload = int( + percentage_to_ranged_value(self._speed_range, percentage) + ) + mqtt_payload = self._command_templates[ATTR_PERCENTAGE](percentage_payload) + if self._implemented_preset_mode: + if percentage: + await self.async_set_preset_mode( + preset_mode=percentage_to_ordered_list_item( + self.speed_list, percentage + ) + ) + # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) + elif self._feature_legacy_speeds and ( + SPEED_OFF in self._legacy_speeds_list + ): + await self.async_set_preset_mode(SPEED_OFF) + # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) + elif self._feature_legacy_speeds: + if percentage: + await self.async_set_speed( + percentage_to_ordered_list_item( + self._legacy_speeds_list_no_off, + percentage, + ) + ) + elif SPEED_OFF in self._legacy_speeds_list: + await self.async_set_speed(SPEED_OFF) + + if self._implemented_percentage: + mqtt.async_publish( + self.hass, + self._topic[CONF_PERCENTAGE_COMMAND_TOPIC], + mqtt_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + if self._optimistic_percentage: + self._percentage = percentage + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan. + + This method is a coroutine. + """ + if preset_mode not in self.preset_modes: + _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) + return + # Legacy are deprecated in the schema, support will be removed after a quarter (2021.7) + if preset_mode in self._legacy_speeds_list: + await self.async_set_speed(speed=preset_mode) + if not self._implemented_percentage and preset_mode in self.speed_list: + self._percentage = ordered_list_item_to_percentage( + self.speed_list, preset_mode + ) + mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) mqtt.async_publish( self.hass, - self._topic[CONF_SPEED_COMMAND_TOPIC], + self._topic[CONF_PRESET_MODE_COMMAND_TOPIC], mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) - if self._optimistic_speed: - self._speed = speed + if self._optimistic_preset_mode: + self._preset_mode = preset_mode + self.async_write_ha_state() + + # async_set_speed is deprecated, support will be removed after a quarter (2021.7) + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan. + + This method is a coroutine. + """ + speed_payload = None + if self._feature_legacy_speeds: + if speed == SPEED_LOW: + speed_payload = self._payload["SPEED_LOW"] + elif speed == SPEED_MEDIUM: + speed_payload = self._payload["SPEED_MEDIUM"] + elif speed == SPEED_HIGH: + speed_payload = self._payload["SPEED_HIGH"] + elif speed == SPEED_OFF: + speed_payload = self._payload["SPEED_OFF"] + else: + _LOGGER.warning("'%s'is not a valid speed", speed) + return + + if speed_payload: + mqtt.async_publish( + self.hass, + self._topic[CONF_SPEED_COMMAND_TOPIC], + speed_payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + ) + + if self._optimistic_speed and speed_payload: + self._speed = speed self.async_write_ha_state() async def async_oscillate(self, oscillating: bool) -> None: @@ -393,15 +724,19 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ - if oscillating is False: - payload = self._payload["OSCILLATE_OFF_PAYLOAD"] + if oscillating: + mqtt_payload = self._command_templates[ATTR_OSCILLATING]( + self._payload["OSCILLATE_ON_PAYLOAD"] + ) else: - payload = self._payload["OSCILLATE_ON_PAYLOAD"] + mqtt_payload = self._command_templates[ATTR_OSCILLATING]( + self._payload["OSCILLATE_OFF_PAYLOAD"] + ) mqtt.async_publish( self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], - payload, + mqtt_payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 412302273ac..95a0cde52f4 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -4,8 +4,9 @@ import functools import voluptuous as vol from homeassistant.components import light +from homeassistant.core import HomeAssistant from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .. import DOMAIN, PLATFORMS from ..mixins import async_setup_entry_helper @@ -31,7 +32,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 light through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 04f01ea0d3a..9c4b0f3a3e3 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -17,12 +17,10 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import ( - CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ON, ) @@ -34,12 +32,7 @@ import homeassistant.util.color as color_util from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages -from ..mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -110,7 +103,6 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -132,7 +124,6 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE @@ -144,8 +135,7 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, } ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) @@ -194,8 +184,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config - topic = { key: config.get(key) for key in ( @@ -540,11 +528,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): return white_value return None - @property - def name(self): - """Return the name of the device if any.""" - return self._config[CONF_NAME] - @property def is_on(self): """Return true if device is on.""" @@ -617,9 +600,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): # If brightness is being used instead of an on command, make sure # there is a brightness input. Either set the brightness to our # saved value or the maximum value if this is the first call - elif on_command_type == "brightness": - if ATTR_BRIGHTNESS not in kwargs: - kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 + elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs: + kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 if ATTR_HS_COLOR in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None: diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8ec5c29db62..8be3708bd61 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -1,4 +1,5 @@ """Support for MQTT JSON lights.""" +from contextlib import suppress import json import logging @@ -6,12 +7,23 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_XY, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, @@ -21,18 +33,17 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + VALID_COLOR_MODES, LightEntity, ) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, - CONF_DEVICE, CONF_EFFECT, CONF_HS, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, - CONF_UNIQUE_ID, CONF_WHITE_VALUE, CONF_XY, STATE_ON, @@ -46,12 +57,7 @@ import homeassistant.util.color as color_util from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages -from ..mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -60,6 +66,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "mqtt_json" DEFAULT_BRIGHTNESS = False +DEFAULT_COLOR_MODE = False DEFAULT_COLOR_TEMP = False DEFAULT_EFFECT = False DEFAULT_FLASH_TIME_LONG = 10 @@ -72,6 +79,9 @@ DEFAULT_XY = False DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 +CONF_COLOR_MODE = "color_mode" +CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" + CONF_EFFECT_LIST = "effect_list" CONF_FLASH_TIME_LONG = "flash_time_long" @@ -80,16 +90,26 @@ CONF_FLASH_TIME_SHORT = "flash_time_short" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" -# Stealing some of these from the base MQTT configs. -PLATFORM_SCHEMA_JSON = ( + +def valid_color_configuration(config): + """Test color_mode is not combined with deprecated config.""" + deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_WHITE_VALUE, CONF_XY} + if config[CONF_COLOR_MODE] and any(config.get(key) for key in deprecated): + raise vol.Invalid(f"color_mode must not be combined with any of {deprecated}") + return config + + +PLATFORM_SCHEMA_JSON = vol.All( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Inclusive( + CONF_COLOR_MODE, "color_mode", default=DEFAULT_COLOR_MODE + ): cv.boolean, vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional( @@ -109,14 +129,16 @@ PLATFORM_SCHEMA_JSON = ( vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( + cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique() + ), vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, - } + }, ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) - .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) + .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema), + valid_color_configuration, ) @@ -138,11 +160,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._topic = None self._optimistic = False self._brightness = None + self._color_mode = None self._color_temp = None self._effect = None - self._hs = None - self._white_value = None self._flash_times = None + self._hs = None + self._rgb = None + self._rgbw = None + self._rgbww = None + self._white_value = None + self._xy = None MqttEntity.__init__(self, None, config, config_entry, discovery_data) @@ -153,8 +180,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config - self._topic = { key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC) } @@ -167,50 +192,92 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): } self._supported_features = SUPPORT_TRANSITION | SUPPORT_FLASH - self._supported_features |= config[CONF_RGB] and SUPPORT_COLOR - self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS - self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP self._supported_features |= config[CONF_EFFECT] and SUPPORT_EFFECT - self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE - self._supported_features |= config[CONF_XY] and SUPPORT_COLOR - self._supported_features |= config[CONF_HS] and SUPPORT_COLOR + if not self._config[CONF_COLOR_MODE]: + self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS + self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP + self._supported_features |= config[CONF_HS] and SUPPORT_COLOR + self._supported_features |= config[CONF_RGB] and ( + SUPPORT_COLOR | SUPPORT_BRIGHTNESS + ) + self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE + self._supported_features |= config[CONF_XY] and SUPPORT_COLOR - def _parse_color(self, values): - try: - red = int(values["color"]["r"]) - green = int(values["color"]["g"]) - blue = int(values["color"]["b"]) + def _update_color(self, values): + if not self._config[CONF_COLOR_MODE]: + # Deprecated color handling + try: + red = int(values["color"]["r"]) + green = int(values["color"]["g"]) + blue = int(values["color"]["b"]) + self._hs = color_util.color_RGB_to_hs(red, green, blue) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid RGB color value received") + return - return color_util.color_RGB_to_hs(red, green, blue) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid RGB color value received") - return self._hs + try: + x_color = float(values["color"]["x"]) + y_color = float(values["color"]["y"]) + self._hs = color_util.color_xy_to_hs(x_color, y_color) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid XY color value received") + return - try: - x_color = float(values["color"]["x"]) - y_color = float(values["color"]["y"]) - - return color_util.color_xy_to_hs(x_color, y_color) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid XY color value received") - return self._hs - - try: - hue = float(values["color"]["h"]) - saturation = float(values["color"]["s"]) - - return (hue, saturation) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid HS color value received") - return self._hs - - return self._hs + try: + hue = float(values["color"]["h"]) + saturation = float(values["color"]["s"]) + self._hs = (hue, saturation) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid HS color value received") + return + else: + color_mode = values["color_mode"] + if not self._supports_color_mode(color_mode): + _LOGGER.warning("Invalid color mode received") + return + try: + if color_mode == COLOR_MODE_COLOR_TEMP: + self._color_temp = int(values["color_temp"]) + self._color_mode = COLOR_MODE_COLOR_TEMP + elif color_mode == COLOR_MODE_HS: + hue = float(values["color"]["h"]) + saturation = float(values["color"]["s"]) + self._color_mode = COLOR_MODE_HS + self._hs = (hue, saturation) + elif color_mode == COLOR_MODE_RGB: + r = int(values["color"]["r"]) # pylint: disable=invalid-name + g = int(values["color"]["g"]) # pylint: disable=invalid-name + b = int(values["color"]["b"]) # pylint: disable=invalid-name + self._color_mode = COLOR_MODE_RGB + self._rgb = (r, g, b) + elif color_mode == COLOR_MODE_RGBW: + r = int(values["color"]["r"]) # pylint: disable=invalid-name + g = int(values["color"]["g"]) # pylint: disable=invalid-name + b = int(values["color"]["b"]) # pylint: disable=invalid-name + w = int(values["color"]["w"]) # pylint: disable=invalid-name + self._color_mode = COLOR_MODE_RGBW + self._rgbw = (r, g, b, w) + elif color_mode == COLOR_MODE_RGBWW: + r = int(values["color"]["r"]) # pylint: disable=invalid-name + g = int(values["color"]["g"]) # pylint: disable=invalid-name + b = int(values["color"]["b"]) # pylint: disable=invalid-name + c = int(values["color"]["c"]) # pylint: disable=invalid-name + w = int(values["color"]["w"]) # pylint: disable=invalid-name + self._color_mode = COLOR_MODE_RGBWW + self._rgbww = (r, g, b, c, w) + elif color_mode == COLOR_MODE_XY: + x = float(values["color"]["x"]) # pylint: disable=invalid-name + y = float(values["color"]["y"]) # pylint: disable=invalid-name + self._color_mode = COLOR_MODE_XY + self._xy = (x, y) + except (KeyError, ValueError): + _LOGGER.warning("Invalid or incomplete color value received") async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -231,7 +298,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if values["color"] is None: self._hs = None else: - self._hs = self._parse_color(values) + self._update_color(values) + + if self._config[CONF_COLOR_MODE] and "color_mode" in values: + self._update_color(values) if self._supported_features and SUPPORT_BRIGHTNESS: try: @@ -245,7 +315,11 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except (TypeError, ValueError): _LOGGER.warning("Invalid brightness value received") - if self._supported_features and SUPPORT_COLOR_TEMP: + if ( + self._supported_features + and SUPPORT_COLOR_TEMP + and not self._config[CONF_COLOR_MODE] + ): try: if values["color_temp"] is None: self._color_temp = None @@ -257,10 +331,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): _LOGGER.warning("Invalid color temp value received") if self._supported_features and SUPPORT_EFFECT: - try: + with suppress(KeyError): self._effect = values["effect"] - except KeyError: - pass if self._supported_features and SUPPORT_WHITE_VALUE: try: @@ -287,16 +359,17 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if self._optimistic and last_state: self._state = last_state.state == STATE_ON - if last_state.attributes.get(ATTR_BRIGHTNESS): - self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) - if last_state.attributes.get(ATTR_HS_COLOR): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) - if last_state.attributes.get(ATTR_COLOR_TEMP): - self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) - if last_state.attributes.get(ATTR_EFFECT): - self._effect = last_state.attributes.get(ATTR_EFFECT) - if last_state.attributes.get(ATTR_WHITE_VALUE): - self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + last_attributes = last_state.attributes + self._brightness = last_attributes.get(ATTR_BRIGHTNESS, self._brightness) + self._color_mode = last_attributes.get(ATTR_COLOR_MODE, self._color_mode) + self._color_temp = last_attributes.get(ATTR_COLOR_TEMP, self._color_temp) + self._effect = last_attributes.get(ATTR_EFFECT, self._effect) + self._hs = last_attributes.get(ATTR_HS_COLOR, self._hs) + self._rgb = last_attributes.get(ATTR_RGB_COLOR, self._rgb) + self._rgbw = last_attributes.get(ATTR_RGBW_COLOR, self._rgbw) + self._rgbww = last_attributes.get(ATTR_RGBWW_COLOR, self._rgbww) + self._white_value = last_attributes.get(ATTR_WHITE_VALUE, self._white_value) + self._xy = last_attributes.get(ATTR_XY_COLOR, self._xy) @property def brightness(self): @@ -333,16 +406,31 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Return the hs color value.""" return self._hs + @property + def rgb_color(self): + """Return the hs color value.""" + return self._rgb + + @property + def rgbw_color(self): + """Return the hs color value.""" + return self._rgbw + + @property + def rgbww_color(self): + """Return the hs color value.""" + return self._rgbww + + @property + def xy_color(self): + """Return the hs color value.""" + return self._xy + @property def white_value(self): """Return the white property.""" return self._white_value - @property - def name(self): - """Return the name of the device if any.""" - return self._config[CONF_NAME] - @property def is_on(self): """Return true if device is on.""" @@ -353,11 +441,45 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Return true if we do optimistic updates.""" return self._optimistic + @property + def color_mode(self): + """Return current color mode.""" + return self._color_mode + + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return self._config.get(CONF_SUPPORTED_COLOR_MODES) + @property def supported_features(self): """Flag supported features.""" return self._supported_features + def _set_flash_and_transition(self, message, **kwargs): + if ATTR_TRANSITION in kwargs: + message["transition"] = kwargs[ATTR_TRANSITION] + + if ATTR_FLASH in kwargs: + flash = kwargs.get(ATTR_FLASH) + + if flash == FLASH_LONG: + message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG] + elif flash == FLASH_SHORT: + message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] + + def _scale_rgbxx(self, rgbxx, kwargs): + # If there's a brightness topic set, we don't want to scale the + # RGBxx values given using the brightness. + if self._config[CONF_BRIGHTNESS]: + brightness = 255 + else: + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + return tuple(round(i / 255 * brightness) for i in rgbxx) + + 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): """Turn the device on. @@ -397,16 +519,53 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._hs = kwargs[ATTR_HS_COLOR] should_update = True - if ATTR_FLASH in kwargs: - flash = kwargs.get(ATTR_FLASH) + if ATTR_HS_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_HS): + hs_color = kwargs[ATTR_HS_COLOR] + message["color"] = {"h": hs_color[0], "s": hs_color[1]} + if self._optimistic: + self._color_mode = COLOR_MODE_HS + self._hs = hs_color + should_update = True - if flash == FLASH_LONG: - message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG] - elif flash == FLASH_SHORT: - message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] + if ATTR_RGB_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_RGB): + rgb = self._scale_rgbxx(kwargs[ATTR_RGB_COLOR], kwargs) + message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2]} + if self._optimistic: + self._color_mode = COLOR_MODE_RGB + self._rgb = rgb + should_update = True - if ATTR_TRANSITION in kwargs: - message["transition"] = kwargs[ATTR_TRANSITION] + if ATTR_RGBW_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_RGBW): + rgb = self._scale_rgbxx(kwargs[ATTR_RGBW_COLOR], kwargs) + message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2], "w": rgb[3]} + if self._optimistic: + self._color_mode = COLOR_MODE_RGBW + self._rgbw = rgb + should_update = True + + if ATTR_RGBWW_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_RGBWW): + rgb = self._scale_rgbxx(kwargs[ATTR_RGBWW_COLOR], kwargs) + message["color"] = { + "r": rgb[0], + "g": rgb[1], + "b": rgb[2], + "c": rgb[3], + "w": rgb[4], + } + if self._optimistic: + self._color_mode = COLOR_MODE_RGBWW + self._rgbww = rgb + should_update = True + + if ATTR_XY_COLOR in kwargs and self._supports_color_mode(COLOR_MODE_XY): + xy = kwargs[ATTR_XY_COLOR] # pylint: disable=invalid-name + message["color"] = {"x": xy[0], "y": xy[1]} + if self._optimistic: + self._color_mode = COLOR_MODE_XY + self._xy = xy + should_update = True + + self._set_flash_and_transition(message, **kwargs) if ATTR_BRIGHTNESS in kwargs and self._config[CONF_BRIGHTNESS]: brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_SCALE @@ -466,8 +625,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """ message = {"state": "OFF"} - if ATTR_TRANSITION in kwargs: - message["transition"] = kwargs[ATTR_TRANSITION] + self._set_flash_and_transition(message, **kwargs) mqtt.async_publish( self.hass, diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 665e1a30d99..7c0266265db 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -21,11 +21,9 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import ( - CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_STATE_TEMPLATE, - CONF_UNIQUE_ID, STATE_OFF, STATE_ON, ) @@ -37,12 +35,7 @@ import homeassistant.util.color as color_util from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages -from ..mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -73,7 +66,6 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template, @@ -83,12 +75,10 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) @@ -127,8 +117,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config - self._topics = { key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC) } @@ -299,11 +287,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Return the white property.""" return self._white_value - @property - def name(self): - """Return the name of the entity.""" - return self._config[CONF_NAME] - @property def is_on(self): """Return True if entity is on.""" @@ -434,7 +417,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): and self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_BLUE_TEMPLATE] is not None ): - features = features | SUPPORT_COLOR + features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS if self._config.get(CONF_EFFECT_LIST) is not None: features = features | SUPPORT_EFFECT if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index e66b93f51c0..cdfa5101548 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -5,13 +5,7 @@ import voluptuous as vol from homeassistant.components import lock from homeassistant.components.lock import LockEntity -from homeassistant.const import ( - CONF_DEVICE, - CONF_NAME, - CONF_OPTIMISTIC, - CONF_UNIQUE_ID, - CONF_VALUE_TEMPLATE, -) +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service @@ -28,13 +22,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -49,26 +37,16 @@ DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_STATE_LOCKED = "LOCKED" DEFAULT_STATE_UNLOCKED = "UNLOCKED" -PLATFORM_SCHEMA = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, - vol.Optional( - CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK - ): cv.string, - vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, - vol.Optional( - CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED - ): cv.string, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, + vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, + vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, + vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -111,8 +89,6 @@ class MqttLock(MqttEntity, LockEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config - self._optimistic = config[CONF_OPTIMISTIC] value_template = self._config.get(CONF_VALUE_TEMPLATE) @@ -153,11 +129,6 @@ class MqttLock(MqttEntity, LockEntity): }, ) - @property - def name(self): - """Return the name of the lock.""" - return self._config[CONF_NAME] - @property def is_locked(self): """Return true if lock is locked.""" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8d9c9533ed3..9b1c7a9fb21 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1,12 +1,13 @@ """MQTT component mixins and helpers.""" +from __future__ import annotations + from abc import abstractmethod import json import logging -from typing import Optional import voluptuous as vol -from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import CONF_DEVICE, CONF_ICON, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -51,6 +52,7 @@ AVAILABILITY_MODES = [AVAILABILITY_ALL, AVAILABILITY_ANY, AVAILABILITY_LATEST] CONF_AVAILABILITY = "availability" CONF_AVAILABILITY_MODE = "availability_mode" CONF_AVAILABILITY_TOPIC = "availability_topic" +CONF_ENABLED_BY_DEFAULT = "enabled_by_default" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" @@ -63,6 +65,7 @@ CONF_MODEL = "model" CONF_SW_VERSION = "sw_version" CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" +CONF_SUGGESTED_AREA = "suggested_area" MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( { @@ -129,15 +132,20 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_SW_VERSION): cv.string, vol.Optional(CONF_VIA_DEVICE): cv.string, + vol.Optional(CONF_SUGGESTED_AREA): cv.string, } ), validate_device_has_at_least_one_identifier, ) -MQTT_JSON_ATTRS_SCHEMA = vol.Schema( +MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( { + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -226,7 +234,7 @@ class MqttAttributes(Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes @@ -347,7 +355,7 @@ async def cleanup_device_registry(hass, device_id): if ( device_id and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id, include_disabled_entities=True + entity_registry, device_id, include_disabled_entities=False ) and not await device_trigger.async_get_triggers(hass, device_id) and not tag.async_has_tags(hass, device_id) @@ -488,13 +496,16 @@ def device_info_from_config(config): if CONF_VIA_DEVICE in config: info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) + if CONF_SUGGESTED_AREA in config: + info["suggested_area"] = config[CONF_SUGGESTED_AREA] + return info class MqttEntityDeviceInfo(Entity): """Mixin used for mqtt platforms that support the device registry.""" - def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: + def __init__(self, device_config: ConfigType | None, config_entry=None) -> None: """Initialize the device mixin.""" self._device_config = device_config self._config_entry = config_entry @@ -527,11 +538,12 @@ class MqttEntity( def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Entity.""" self.hass = hass + self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None # Load config - self._setup_from_config(config) + self._setup_from_config(self._config) # Initialize mixin classes MqttAttributes.__init__(self, config) @@ -547,7 +559,8 @@ class MqttEntity( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = self.config_schema()(discovery_payload) - self._setup_from_config(config) + self._config = config + self._setup_from_config(self._config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) @@ -575,6 +588,21 @@ class MqttEntity( async def _subscribe_topics(self): """(Re)Subscribe to topics.""" + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._config[CONF_ENABLED_BY_DEFAULT] + + @property + def icon(self): + """Return icon of the entity if any.""" + return self._config.get(CONF_ICON) + + @property + def name(self): + """Return the name of the device if any.""" + return self._config.get(CONF_NAME) + @property def should_poll(self): """No polling needed.""" diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 202f457372c..7cdafeef98d 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,6 +1,8 @@ """Modesl used by multiple MQTT modules.""" +from __future__ import annotations + import datetime as dt -from typing import Callable, Optional, Union +from typing import Callable, Union import attr @@ -15,8 +17,8 @@ class Message: payload: PublishPayloadType = attr.ib() qos: int = attr.ib() retain: bool = attr.ib() - subscribed_topic: Optional[str] = attr.ib(default=None) - timestamp: Optional[dt.datetime] = attr.ib(default=None) + subscribed_topic: str | None = attr.ib(default=None) + timestamp: dt.datetime | None = attr.ib(default=None) MessageCallbackType = Callable[[Message], None] diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index aa24f81eb69..e7839f8e483 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -6,13 +6,7 @@ import voluptuous as vol from homeassistant.components import number from homeassistant.components.number import NumberEntity -from homeassistant.const import ( - CONF_DEVICE, - CONF_ICON, - CONF_NAME, - CONF_OPTIMISTIC, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service @@ -30,32 +24,19 @@ from . import ( from .. import mqtt from .const import CONF_RETAIN from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "MQTT Number" DEFAULT_OPTIMISTIC = False -PLATFORM_SCHEMA = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -90,7 +71,6 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._current_number = None self._optimistic = config.get(CONF_OPTIMISTIC) - self._unique_id = config.get(CONF_UNIQUE_ID) NumberEntity.__init__(self) MqttEntity.__init__(self, None, config, config_entry, discovery_data) @@ -100,9 +80,6 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """Return the config schema.""" return PLATFORM_SCHEMA - def _setup_from_config(self, config): - self._config = config - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -165,17 +142,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._config[CONF_RETAIN], ) - @property - def name(self): - """Return the name of this number.""" - return self._config[CONF_NAME] - @property def assumed_state(self): """Return true if we do optimistic updates.""" return self._optimistic - - @property - def icon(self): - """Return the icon.""" - return self._config.get(CONF_ICON) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d017cb2ce85..65c9e0550e0 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,25 +1,22 @@ """Support for MQTT sensors.""" +from __future__ import annotations + from datetime import timedelta import functools -from typing import Optional import voluptuous as vol from homeassistant.components import sensor -from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, SensorEntity from homeassistant.const import ( - CONF_DEVICE, CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, - CONF_ICON, CONF_NAME, - CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity 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 @@ -29,9 +26,7 @@ from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription from .. import mqtt from .debug_info import log_messages from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, + MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, async_setup_entry_helper, @@ -41,22 +36,15 @@ CONF_EXPIRE_AFTER = "expire_after" DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False -PLATFORM_SCHEMA = ( - mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -82,7 +70,7 @@ async def _async_setup_entity( async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) -class MqttSensor(MqttEntity, Entity): +class MqttSensor(MqttEntity, SensorEntity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): @@ -105,7 +93,6 @@ class MqttSensor(MqttEntity, Entity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass @@ -163,11 +150,6 @@ class MqttSensor(MqttEntity, Entity): self._expired = True self.async_write_ha_state() - @property - def name(self): - """Return the name of the sensor.""" - return self._config[CONF_NAME] - @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" @@ -184,12 +166,7 @@ class MqttSensor(MqttEntity, Entity): return self._state @property - def icon(self): - """Return the icon.""" - return self._config.get(CONF_ICON) - - @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) @@ -197,7 +174,6 @@ class MqttSensor(MqttEntity, Entity): def available(self) -> bool: """Return true if the device is available and value has not expired.""" expire_after = self._config.get(CONF_EXPIRE_AFTER) - # pylint: disable=no-member return MqttAvailability.available.fget(self) and ( expire_after is None or not self._expired ) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 8c3db8e5b61..2edbc86eb8c 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -12,8 +12,8 @@ } }, "hassio_confirm": { - "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 Hass.io add-on {addon}?", + "title": "MQTT Broker via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?", "data": { "discovery": "Enable discovery" } diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 5c2efabc266..e6c99c09fd5 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -1,5 +1,7 @@ """Helper to handle a set of topics to subscribe to.""" -from typing import Any, Callable, Dict, Optional +from __future__ import annotations + +from typing import Any, Callable import attr @@ -19,7 +21,7 @@ class EntitySubscription: hass: HomeAssistantType = attr.ib() topic: str = attr.ib() message_callback: MessageCallbackType = attr.ib() - unsubscribe_callback: Optional[Callable[[], None]] = attr.ib() + unsubscribe_callback: Callable[[], None] | None = attr.ib() qos: int = attr.ib(default=0) encoding: str = attr.ib(default="utf-8") @@ -62,8 +64,8 @@ class EntitySubscription: @bind_hass async def async_subscribe_topics( hass: HomeAssistantType, - new_state: Optional[Dict[str, EntitySubscription]], - topics: Dict[str, Any], + new_state: dict[str, EntitySubscription] | None, + topics: dict[str, Any], ): """(Re)Subscribe to a set of MQTT topics. diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 939d6bb98b1..2b272b0f9be 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -6,13 +6,10 @@ import voluptuous as vol from homeassistant.components import switch from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( - CONF_DEVICE, - CONF_ICON, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ON, ) @@ -33,13 +30,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, - async_setup_entry_helper, -) +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -48,23 +39,16 @@ DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" -PLATFORM_SCHEMA = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, - vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, - vol.Optional(CONF_STATE_OFF): cv.string, - vol.Optional(CONF_STATE_ON): cv.string, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) -) +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_STATE_OFF): cv.string, + vol.Optional(CONF_STATE_ON): cv.string, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( @@ -110,8 +94,6 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): def _setup_from_config(self, config): """(Re)Setup the entity.""" - self._config = config - state_on = config.get(CONF_STATE_ON) self._state_on = state_on if state_on else config[CONF_PAYLOAD_ON] @@ -163,11 +145,6 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): if last_state: self._state = last_state.state == STATE_ON - @property - def name(self): - """Return the name of the switch.""" - return self._config[CONF_NAME] - @property def is_on(self): """Return true if device is on.""" @@ -178,11 +155,6 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Return true if we do optimistic updates.""" return self._optimistic - @property - def icon(self): - """Return the icon.""" - return self._config.get(CONF_ICON) - async def async_turn_on(self, **kwargs): """Turn the device on. diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 7a470b74272..96343a7f87a 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 MQTT \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 {addon}?", - "title": "MQTT \u0431\u0440\u043e\u043a\u0435\u0440 \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 MQTT \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 {addon}?", + "title": "MQTT \u0431\u0440\u043e\u043a\u0435\u0440 \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430" } } } diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index f72ee30cdcf..23b7cd5dfa9 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 a trav\u00e9s del complement de Hass.io" + "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" } } }, diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index 60c323d9051..9876e2509b3 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -21,8 +21,8 @@ "data": { "discovery": "Povolit automatick\u00e9 vyhled\u00e1v\u00e1n\u00ed za\u0159\u00edzen\u00ed" }, - "description": "Chcete nakonfigurovat Home Assistant pro p\u0159ipojen\u00ed k MQTT poskytovan\u00e9mu dopl\u0148kem {addon} z Hass.io?", - "title": "MQTT Broker prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + "description": "Chcete nakonfigurovat Home Assistant pro p\u0159ipojen\u00ed k MQTT poskytovan\u00e9mu dopl\u0148kem {addon} z Supervisor?", + "title": "MQTT Broker prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" } } }, diff --git a/homeassistant/components/mqtt/translations/da.json b/homeassistant/components/mqtt/translations/da.json index 7ff0f2b0a70..9b853a2dae2 100644 --- a/homeassistant/components/mqtt/translations/da.json +++ b/homeassistant/components/mqtt/translations/da.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktiv\u00e9r opdagelse" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT-brokeren, der leveres af hass.io-tilf\u00f8jelsen {addon}?", - "title": "MQTT-broker via Hass.io-tilf\u00f8jelse" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT-brokeren, der leveres af Supervisor-tilf\u00f8jelsen {addon}?", + "title": "MQTT-broker via Supervisor-tilf\u00f8jelse" } } }, diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 3346abfd53e..4b57249eb38 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -21,8 +21,8 @@ "data": { "discovery": "Suche aktivieren" }, - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?", - "title": "MQTT Broker per Hass.io add-on" + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Supervisor Add-on {addon} bereitgestellt wird?", + "title": "MQTT Broker per Supervisor add-on" } } }, diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index d2ddc6691d1..a69be795f77 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -21,8 +21,8 @@ "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfDesea configurar el Asistente del Hogar para que se conecte al broker MQTT proporcionado por el complemento hass.io {addon}?", - "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" + "description": "\u00bfDesea configurar el Asistente del Hogar para que se conecte al broker MQTT proporcionado por el complemento Supervisor {addon}?", + "title": "MQTT Broker a trav\u00e9s del complemento Supervisor" } } }, diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index d36cfbc9694..70107efa269 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -21,8 +21,8 @@ "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfQuieres configurar Home Assistant para conectar con el broker de MQTT proporcionado por el complemento Hass.io {addon}?", - "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" + "description": "\u00bfQuieres configurar Home Assistant para conectar con el broker de MQTT proporcionado por el complemento Supervisor {addon}?", + "title": "MQTT Broker a trav\u00e9s del complemento Supervisor" } } }, diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index d2b863cab46..f28b1f4f94e 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 soovite seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?", - "title": "MQTT vahendaja Hass.io pistikprogrammi kaudu" + "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?", + "title": "MQTT vahendaja Hass.io lisandmooduli abil" } } }, diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 574db2d2faf..6ee3788725d 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -21,8 +21,8 @@ "data": { "discovery": "Activer la d\u00e9couverte" }, - "description": "Vous voulez configurer Home Assistant pour vous connecter au broker MQTT fourni par l\u2019Add-on hass.io {addon} ?", - "title": "MQTT Broker via le module compl\u00e9mentaire Hass.io" + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte au courtier MQTT fourni par le module compl\u00e9mentaire Hass.io {addon} ?", + "title": "Courtier MQTT via le module compl\u00e9mentaire Hass.io" } } }, diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 8cc6aa0857f..f265789d777 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Csak egyetlen MQTT konfigur\u00e1ci\u00f3 megengedett." + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a br\u00f3kerhez." + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "broker": { @@ -21,8 +21,8 @@ "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, - "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Hass.io add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?", - "title": "MQTT Br\u00f3ker a Hass.io b\u0151v\u00edtm\u00e9nnyel" + "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Supervisor add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?", + "title": "MQTT Br\u00f3ker a Supervisor b\u0151v\u00edtm\u00e9nnyel" } } }, @@ -47,5 +47,20 @@ "button_short_release": "\"{subtype}\" felengedve", "button_triple_press": "\"{subtype}\" tripla kattint\u00e1s" } + }, + "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "broker": { + "data": { + "broker": "Br\u00f3ker", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index e21052a501f..2a3171456c8 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -1,20 +1,72 @@ { "config": { "abort": { - "single_instance_allowed": "Hanya satu konfigurasi MQTT yang diizinkan." + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { - "cannot_connect": "Tidak dapat terhubung ke broker." + "cannot_connect": "Gagal terhubung" }, "step": { "broker": { "data": { "broker": "Broker", - "password": "Kata sandi", + "discovery": "Aktifkan penemuan", + "password": "Kata Sandi", "port": "Port", - "username": "Nama pengguna" + "username": "Nama Pengguna" }, - "description": "Harap masukkan informasi koneksi dari broker MQTT Anda." + "description": "Masukkan informasi koneksi broker MQTT Anda." + }, + "hassio_confirm": { + "data": { + "discovery": "Aktifkan penemuan" + }, + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke broker MQTT yang disediakan oleh add-on Supervisor {addon}?", + "title": "MQTT Broker via add-on Supervisor" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Tombol pertama", + "button_2": "Tombol kedua", + "button_3": "Tombol ketiga", + "button_4": "Tombol keempat", + "button_5": "Tombol kelima", + "button_6": "Tombol keenam", + "turn_off": "Matikan", + "turn_on": "Nyalakan" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" diklik dua kali", + "button_long_press": "\"{subtype}\" terus ditekan", + "button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "button_quadruple_press": "\"{subtype}\" diklik empat kali", + "button_quintuple_press": "\"{subtype}\" diklik lima kali", + "button_short_press": "\"{subtype}\" ditekan", + "button_short_release": "\"{subtype}\" dilepas", + "button_triple_press": "\"{subtype}\" diklik tiga kali" + } + }, + "options": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "description": "Masukkan informasi koneksi broker MQTT Anda." + }, + "options": { + "data": { + "discovery": "Aktifkan penemuan" + }, + "description": "Pilih opsi MQTT." } } } diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index fd79863fadf..e7631c5805d 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" @@ -21,7 +21,7 @@ "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?", + "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" } } @@ -38,14 +38,14 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "button_double_press": "\"{subtype}\" \uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", - "button_long_press": "\"{subtype}\" \uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", - "button_long_release": "\"{subtype}\" \uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", - "button_quadruple_press": "\"{subtype}\" \uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", - "button_quintuple_press": "\"{subtype}\" \uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", - "button_short_press": "\"{subtype}\" \uc774 \ub20c\ub9b4 \ub54c", - "button_short_release": "\"{subtype}\" \uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", - "button_triple_press": "\"{subtype}\" \uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" + "button_double_press": "\"{subtype}\"\uc774(\uac00) \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c", + "button_long_press": "\"{subtype}\"\uc774(\uac00) \uacc4\uc18d \ub20c\ub838\uc744 \ub54c", + "button_long_release": "\"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c", + "button_quadruple_press": "\"{subtype}\"\uc774(\uac00) \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c", + "button_quintuple_press": "\"{subtype}\"\uc774(\uac00) \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c", + "button_short_press": "\"{subtype}\"\uc774(\uac00) \ub20c\ub838\uc744 \ub54c", + "button_short_release": "\"{subtype}\"\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c", + "button_triple_press": "\"{subtype}\"\uc774(\uac00) \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c" } }, "options": { @@ -66,12 +66,13 @@ }, "options": { "data": { + "birth_enable": "Birth \uba54\uc2dc\uc9c0 \ud65c\uc131\ud654\ud558\uae30", "birth_payload": "Birth \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", "birth_qos": "Birth \uba54\uc2dc\uc9c0 QoS", "birth_retain": "Birth \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", "birth_topic": "Birth \uba54\uc2dc\uc9c0 \ud1a0\ud53d", - "discovery": "\uc7a5\uce58 \uac80\uc0c9 \ud65c\uc131\ud654", - "will_enable": "Will \uba54\uc2dc\uc9c0 \ud65c\uc131\ud654", + "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654\ud558\uae30", + "will_enable": "Will \uba54\uc2dc\uc9c0 \ud65c\uc131\ud654\ud558\uae30", "will_payload": "Will \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", "will_qos": "Will \uba54\uc2dc\uc9c0 QoS", "will_retain": "Will \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json index 88a2664cd37..fd9cd351858 100644 --- a/homeassistant/components/mqtt/translations/lb.json +++ b/homeassistant/components/mqtt/translations/lb.json @@ -21,8 +21,8 @@ "data": { "discovery": "Entdeckung aktiv\u00e9ieren" }, - "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam MQTT broker ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", - "title": "MQTT Broker via Hass.io add-on" + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam MQTT broker ze verbannen dee vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?", + "title": "MQTT Broker via Supervisor add-on" } } }, diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 3b3ebf9fe3b..cac483b1bf0 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van MQTT is toegestaan." + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "error": { - "cannot_connect": "Kan geen verbinding maken met de broker." + "cannot_connect": "Kan geen verbinding maken" }, "step": { "broker": { @@ -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 hass.io add-on {addon} ?", - "title": "MQTTT Broker via Hass.io add-on" + "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" } } }, @@ -50,11 +50,14 @@ }, "options": { "error": { + "bad_birth": "Ongeldig birth topic", + "bad_will": "Ongeldig will topic", "cannot_connect": "Kon niet verbinden" }, "step": { "broker": { "data": { + "broker": "Broker", "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" @@ -65,8 +68,17 @@ "data": { "birth_enable": "Geboortebericht inschakelen", "birth_payload": "Birth message payload", - "birth_topic": "Birth message onderwerp" - } + "birth_qos": "Birth message QoS", + "birth_retain": "Birth message behouden", + "birth_topic": "Birth message onderwerp", + "discovery": "Discovery inschakelen", + "will_enable": "Will message inschakelen", + "will_payload": "Will message payload", + "will_qos": "Will message QoS", + "will_retain": "Will message behouden", + "will_topic": "Will message topic" + }, + "description": "Selecteer MQTT-opties." } } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 12c72603a1f..586c62dac6a 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 en MQTT megler som er levert av Hass.io-tillegg {addon}?", - "title": "MQTT megler via Hass.io-tillegg" + "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" } } }, diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 08b1d2f1974..287f0165d96 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -22,7 +22,7 @@ "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 za po\u015brednictwem dodatku Hass.io" + "title": "Po\u015brednik MQTT przez dodatek Hass.io" } } }, diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index de739963cae..ef9fad14440 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -21,8 +21,8 @@ "data": { "discovery": "Ativar descoberta" }, - "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo complemento hass.io {addon}?", - "title": "MQTT Broker via add-on Hass.io" + "description": "Deseja configurar o Home Assistant para se conectar ao broker MQTT fornecido pelo complemento Supervisor {addon}?", + "title": "MQTT Broker via add-on Supervisor" } } }, diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index 606997038b2..209c33cf165 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -21,8 +21,8 @@ "data": { "discovery": "Ativar descoberta" }, - "description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on hass.io {addon}?", - "title": "MQTT Broker atrav\u00e9s do add-on Hass.io" + "description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on Supervisor {addon}?", + "title": "MQTT Broker atrav\u00e9s do add-on Supervisor" } } }, diff --git a/homeassistant/components/mqtt/translations/ro.json b/homeassistant/components/mqtt/translations/ro.json index 2292b58d01d..a98818be937 100644 --- a/homeassistant/components/mqtt/translations/ro.json +++ b/homeassistant/components/mqtt/translations/ro.json @@ -22,7 +22,7 @@ "discovery": "Activa\u021bi descoperirea" }, "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a se conecta la brokerul MQTT furnizat de addon-ul {addon} ?", - "title": "MQTT Broker, prin intermediul Hass.io add-on" + "title": "MQTT Broker, prin intermediul Supervisor add-on" } } } diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 7cc7a84b28c..4357a0902c6 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -13,7 +13,7 @@ "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", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT." }, @@ -60,7 +60,7 @@ "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT." }, diff --git a/homeassistant/components/mqtt/translations/sl.json b/homeassistant/components/mqtt/translations/sl.json index afd2f3b8000..9f16209d524 100644 --- a/homeassistant/components/mqtt/translations/sl.json +++ b/homeassistant/components/mqtt/translations/sl.json @@ -21,8 +21,8 @@ "data": { "discovery": "Omogo\u010di odkrivanje" }, - "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s posrednikom MQTT, ki ga ponuja dodatek Hass.io {addon} ?", - "title": "MQTT Broker prek dodatka Hass.io" + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s posrednikom MQTT, ki ga ponuja dodatek Supervisor {addon} ?", + "title": "MQTT Broker prek dodatka Supervisor" } } }, diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index c74979bb6bb..b3088ca49a9 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktivera uppt\u00e4ckt" }, - "description": "Vill du konfigurera Home Assistant att ansluta till den MQTT-broker som tillhandah\u00e5lls av Hass.io-till\u00e4gget \"{addon}\"?", - "title": "MQTT Broker via Hass.io till\u00e4gg" + "description": "Vill du konfigurera Home Assistant att ansluta till den MQTT-broker som tillhandah\u00e5lls av Supervisor-till\u00e4gget \"{addon}\"?", + "title": "MQTT Broker via Supervisor till\u00e4gg" } } }, diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index f871db4aa9d..b8cbab32b14 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" }, - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" } } }, diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index 63ceded5654..97356ed44d4 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u542f\u7528\u53d1\u73b0" }, - "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Hass.io \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f", - "title": "\u6765\u81ea Hass.io \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668" + "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Supervisor \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f", + "title": "\u6765\u81ea Supervisor \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668" } } }, diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index bfb27361889..807de2e2c09 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\u6574\u5408 {addon} \u4e4b MQTT broker\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 MQTT Broker" + "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" } } }, diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 82f7885b85d..34c47aec791 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -1,4 +1,5 @@ """Offer MQTT listening automation rules.""" +from contextlib import suppress import json import logging @@ -36,6 +37,7 @@ _LOGGER = logging.getLogger(__name__) async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None topic = config[CONF_TOPIC] wanted_payload = config.get(CONF_PAYLOAD) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -77,12 +79,11 @@ async def async_attach_trigger(hass, config, action, automation_info): "payload": mqttmsg.payload, "qos": mqttmsg.qos, "description": f"mqtt topic {mqttmsg.topic}", + "id": trigger_id, } - try: + with suppress(ValueError): data["payload_json"] = json.loads(mqttmsg.payload) - except ValueError: - pass hass.async_run_hass_job(job, {"trigger": data}) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index ca91b8948d4..f0f00a72bb4 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -17,12 +17,7 @@ from homeassistant.components.vacuum import ( SUPPORT_TURN_ON, VacuumEntity, ) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, - CONF_DEVICE, - CONF_NAME, - CONF_UNIQUE_ID, -) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level @@ -30,12 +25,7 @@ from homeassistant.helpers.icon import icon_for_battery_level from .. import subscription from ... import mqtt from ..debug_info import log_messages -from ..mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services SERVICE_TO_STRING = { @@ -117,7 +107,6 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, @@ -152,13 +141,11 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) @@ -192,7 +179,6 @@ class MqttVacuum(MqttEntity, VacuumEntity): return PLATFORM_SCHEMA_LEGACY def _setup_from_config(self, config): - self._name = config[CONF_NAME] supported_feature_strings = config[CONF_SUPPORTED_FEATURES] self._supported_features = strings_to_services( supported_feature_strings, STRING_TO_SERVICE @@ -338,11 +324,6 @@ class MqttVacuum(MqttEntity, VacuumEntity): }, ) - @property - def name(self): - """Return the name of the vacuum.""" - return self._name - @property def is_on(self): """Return true if vacuum is on.""" diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 3e43736ab2e..37a12d33df6 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -22,24 +22,14 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, - CONF_DEVICE, - CONF_NAME, - CONF_UNIQUE_ID, -) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages -from ..mixins import ( - MQTT_AVAILABILITY_SCHEMA, - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MQTT_JSON_ATTRS_SCHEMA, - MqttEntity, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services SERVICE_TO_STRING = { @@ -113,7 +103,6 @@ DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -136,13 +125,11 @@ PLATFORM_SCHEMA_STATE = ( vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(MQTT_AVAILABILITY_SCHEMA.schema) - .extend(MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) @@ -171,8 +158,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): return PLATFORM_SCHEMA_STATE def _setup_from_config(self, config): - self._config = config - self._name = config[CONF_NAME] supported_feature_strings = config[CONF_SUPPORTED_FEATURES] self._supported_features = strings_to_services( supported_feature_strings, STRING_TO_SERVICE @@ -219,11 +204,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): self.hass, self._sub_state, topics ) - @property - def name(self): - """Return the name of the vacuum.""" - return self._name - @property def state(self): """Return state of vacuum.""" diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index 5a5f3b3c74d..328b9395eea 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -60,13 +60,13 @@ async def async_setup(hass, config): # Filter out the events that were triggered by publishing # to the MQTT topic, or you will end up in an infinite loop. - if event.event_type == EVENT_CALL_SERVICE: - if ( - event.data.get("domain") == mqtt.DOMAIN - and event.data.get("service") == mqtt.SERVICE_PUBLISH - and event.data[ATTR_SERVICE_DATA].get("topic") == pub_topic - ): - return + if ( + event.event_type == EVENT_CALL_SERVICE + and event.data.get("domain") == mqtt.DOMAIN + and event.data.get("service") == mqtt.SERVICE_PUBLISH + and event.data[ATTR_SERVICE_DATA].get("topic") == pub_topic + ): + return event_info = {"event_type": event.event_type, "event_data": event.data} msg = json.dumps(event_info, cls=JSONEncoder) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 0d07133b396..e446ab8ba7a 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -7,20 +7,24 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import CONF_STATE_TOPIC -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ID, CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ID, + CONF_DEVICE_ID, + CONF_NAME, + CONF_TIMEOUT, + STATE_NOT_HOME, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import dt, slugify _LOGGER = logging.getLogger(__name__) -ATTR_DEVICE_ID = "device_id" ATTR_DISTANCE = "distance" ATTR_ROOM = "room" -CONF_DEVICE_ID = "device_id" CONF_AWAY_TIMEOUT = "away_timeout" DEFAULT_AWAY_TIMEOUT = 0 @@ -66,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class MQTTRoomSensor(Entity): +class MQTTRoomSensor(SensorEntity): """Representation of a room sensor that is updated via MQTT.""" def __init__(self, name, state_topic, device_id, timeout, consider_home): @@ -130,7 +134,7 @@ class MQTTRoomSensor(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_DISTANCE: self._distance} diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 20a7092d58a..541c6075cc3 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -8,7 +8,6 @@ from mullvad_api import MullvadAPI from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from .const import DOMAIN @@ -36,16 +35,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict): update_method=async_get_mullvad_api_data, update_interval=timedelta(minutes=1), ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -56,8 +52,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py index f85820cd7d0..1a91bba2038 100644 --- a/homeassistant/components/mullvad/binary_sensor.py +++ b/homeassistant/components/mullvad/binary_sensor.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MullvadBinarySensor(CoordinatorEntity, BinarySensorEntity): """Represents a Mullvad binary sensor.""" - def __init__(self, coordinator, sensor): # pylint: disable=super-init-not-called + def __init__(self, coordinator, sensor): """Initialize the Mullvad binary sensor.""" super().__init__(coordinator) self.id = sensor[CONF_ID] diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index 674308c1d1a..50f67d10e25 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -5,7 +5,7 @@ from mullvad_api import MullvadAPI, MullvadAPIError from homeassistant import config_entries -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mullvad/translations/bg.json b/homeassistant/components/mullvad/translations/bg.json new file mode 100644 index 00000000000..a84e1c3bfdf --- /dev/null +++ b/homeassistant/components/mullvad/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/de.json b/homeassistant/components/mullvad/translations/de.json index 625c7372347..6014a9155c8 100644 --- a/homeassistant/components/mullvad/translations/de.json +++ b/homeassistant/components/mullvad/translations/de.json @@ -14,7 +14,8 @@ "host": "Host", "password": "Passwort", "username": "Benutzername" - } + }, + "description": "Mullvad VPN Integration einrichten?" } } } diff --git a/homeassistant/components/mullvad/translations/el.json b/homeassistant/components/mullvad/translations/el.json new file mode 100644 index 00000000000..6f19f0039ed --- /dev/null +++ b/homeassistant/components/mullvad/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u0386\u03ba\u03c5\u03c1\u03b7 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "host": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Mullvad VPN;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index d6a17561c3d..579726b061e 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Fallo al conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, "description": "\u00bfConfigurar la integraci\u00f3n VPN de Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/hu.json b/homeassistant/components/mullvad/translations/hu.json new file mode 100644 index 00000000000..0abcc301f0c --- /dev/null +++ b/homeassistant/components/mullvad/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/id.json b/homeassistant/components/mullvad/translations/id.json new file mode 100644 index 00000000000..a5409549f19 --- /dev/null +++ b/homeassistant/components/mullvad/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" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Siapkan integrasi VPN Mullvad?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ko.json b/homeassistant/components/mullvad/translations/ko.json new file mode 100644 index 00000000000..fd9134b977c --- /dev/null +++ b/homeassistant/components/mullvad/translations/ko.json @@ -0,0 +1,22 @@ +{ + "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", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Mullvad VPN \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/pl.json b/homeassistant/components/mullvad/translations/pl.json new file mode 100644 index 00000000000..f5aca4e092c --- /dev/null +++ b/homeassistant/components/mullvad/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": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Skonfigurowa\u0107 integracj\u0119 Mullvad VPN?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/pt.json b/homeassistant/components/mullvad/translations/pt.json new file mode 100644 index 00000000000..561c8d77287 --- /dev/null +++ b/homeassistant/components/mullvad/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ru.json b/homeassistant/components/mullvad/translations/ru.json index ff34530e4a9..af2ecd321f0 100644 --- a/homeassistant/components/mullvad/translations/ru.json +++ b/homeassistant/components/mullvad/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN." } diff --git a/homeassistant/components/mullvad/translations/zh-Hans.json b/homeassistant/components/mullvad/translations/zh-Hans.json new file mode 100644 index 00000000000..acb02a7d0f6 --- /dev/null +++ b/homeassistant/components/mullvad/translations/zh-Hans.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u5b8c\u6210\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u9a8c\u8bc1\u5931\u8d25", + "unknown": "\u9884\u671f\u5916\u7684\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 2ceca024a6f..16b061a3346 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -6,10 +6,9 @@ import logging import MVGLive import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -78,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class MVGLiveSensor(Entity): +class MVGLiveSensor(SensorEntity): """Implementation of an MVG Live sensor.""" def __init__( @@ -114,7 +113,7 @@ class MVGLiveSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" dep = self.data.departures if not dep: diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py index 41311845185..18b5e95d838 100644 --- a/homeassistant/components/mychevy/sensor.py +++ b/homeassistant/components/mychevy/sensor.py @@ -1,10 +1,9 @@ """Support for MyChevy sensors.""" import logging -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.const import PERCENTAGE from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify @@ -46,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class MyChevyStatus(Entity): +class MyChevyStatus(SensorEntity): """A string representing the charge mode.""" _name = "MyChevy Status" @@ -109,7 +108,7 @@ class MyChevyStatus(Entity): return False -class EVSensor(Entity): +class EVSensor(SensorEntity): """Base EVSensor class. The only real difference between sensors is which units and what @@ -160,6 +159,8 @@ class EVSensor(Entity): """Update state.""" if self._car is not None: self._state = getattr(self._car, self._attr, None) + if self._unit_of_measurement == "miles": + self._state = round(self._state) for attr in self._extra_attrs: self._state_attributes[attr] = getattr(self._car, attr) self.async_write_ha_state() @@ -170,7 +171,7 @@ class EVSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return all the state attributes.""" return self._state_attributes diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 6b3a52ba7b0..b25751d7270 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -58,9 +58,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -71,8 +71,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 352283607d8..17c98195a4e 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/myq/translations/he.json b/homeassistant/components/myq/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/myq/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json index dee4ed9ee0f..9c5b90e7447 100644 --- a/homeassistant/components/myq/translations/hu.json +++ b/homeassistant/components/myq/translations/hu.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s 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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/myq/translations/id.json b/homeassistant/components/myq/translations/id.json new file mode 100644 index 00000000000..2cc790d15e0 --- /dev/null +++ b/homeassistant/components/myq/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke MyQ Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json index fd6310cce6a..65df320a544 100644 --- a/homeassistant/components/myq/translations/nl.json +++ b/homeassistant/components/myq/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "MyQ is al geconfigureerd" + "already_configured": "Service is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/myq/translations/ru.json b/homeassistant/components/myq/translations/ru.json index c3b113f148f..c88db7d6960 100644 --- a/homeassistant/components/myq/translations/ru.json +++ b/homeassistant/components/myq/translations/ru.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "MyQ" } diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 0f8123e3a31..c9ad496762d 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,8 +1,10 @@ """Connect to a MySensors gateway via pymysensors API.""" +from __future__ import annotations + import asyncio from functools import partial import logging -from typing import Callable, Dict, List, Optional, Tuple, Type, Union +from typing import Callable from mysensors import BaseAsyncGateway import voluptuous as vol @@ -36,7 +38,7 @@ from .const import ( MYSENSORS_DISCOVERY, MYSENSORS_GATEWAYS, MYSENSORS_ON_UNLOAD, - SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT, + PLATFORMS_WITH_ENTRY_SUPPORT, DevId, SensorType, ) @@ -222,7 +224,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await asyncio.gather( *[ hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT + for platform in PLATFORMS_WITH_ENTRY_SUPPORT ] ) await finish_setup(hass, entry, gateway) @@ -241,7 +243,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT + for platform in PLATFORMS_WITH_ENTRY_SUPPORT ] ) ) @@ -265,13 +267,13 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo def setup_mysensors_platform( hass: HomeAssistant, domain: str, # hass platform name - discovery_info: Dict[str, List[DevId]], - device_class: Union[Type[MySensorsDevice], Dict[SensorType, Type[MySensorsEntity]]], - device_args: Optional[ - Tuple - ] = None, # extra arguments that will be given to the entity constructor - async_add_entities: Optional[Callable] = None, -) -> Optional[List[MySensorsDevice]]: + discovery_info: dict[str, list[DevId]], + device_class: type[MySensorsDevice] | dict[SensorType, type[MySensorsEntity]], + device_args: ( + None | tuple + ) = None, # extra arguments that will be given to the entity constructor + async_add_entities: Callable | None = None, +) -> list[MySensorsDevice] | None: """Set up a MySensors platform. Sets up a bunch of instances of a single platform that is supported by this integration. @@ -281,10 +283,10 @@ def setup_mysensors_platform( """ if device_args is None: device_args = () - new_devices: List[MySensorsDevice] = [] - new_dev_ids: List[DevId] = discovery_info[ATTR_DEVICES] + new_devices: list[MySensorsDevice] = [] + new_dev_ids: list[DevId] = discovery_info[ATTR_DEVICES] for dev_id in new_dev_ids: - devices: Dict[DevId, MySensorsDevice] = get_mysensors_devices(hass, domain) + devices: dict[DevId, MySensorsDevice] = get_mysensors_devices(hass, domain) if dev_id in devices: _LOGGER.debug( "Skipping setup of %s for platform %s as it already exists", @@ -293,7 +295,7 @@ def setup_mysensors_platform( ) continue gateway_id, node_id, child_id, value_type = dev_id - gateway: Optional[BaseAsyncGateway] = get_mysensors_gateway(hass, gateway_id) + gateway: BaseAsyncGateway | None = get_mysensors_gateway(hass, gateway_id) if not gateway: _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id) continue diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index d2cd7f3bccd..bdf1b9392a8 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -1,7 +1,10 @@ """Config flow for MySensors.""" +from __future__ import annotations + +from contextlib import suppress import logging import os -from typing import Any, Dict, Optional +from typing import Any from awesomeversion import ( AwesomeVersion, @@ -23,8 +26,6 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION - -# pylint: disable=unused-import from .const import ( CONF_BAUD_RATE, CONF_GATEWAY_TYPE, @@ -44,7 +45,7 @@ from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_conn _LOGGER = logging.getLogger(__name__) -def _get_schema_common(user_input: Dict[str, str]) -> dict: +def _get_schema_common(user_input: dict[str, str]) -> dict: """Create a schema with options common to all gateway types.""" schema = { vol.Required( @@ -59,25 +60,24 @@ def _get_schema_common(user_input: Dict[str, str]) -> dict: return schema -def _validate_version(version: str) -> Dict[str, str]: +def _validate_version(version: str) -> dict[str, str]: """Validate a version string from the user.""" version_okay = False - try: + with suppress(AwesomeVersionStrategyException): version_okay = bool( AwesomeVersion.ensure_strategy( version, [AwesomeVersionStrategy.SIMPLEVER, AwesomeVersionStrategy.SEMVER], ) ) - except AwesomeVersionStrategyException: - pass + if version_okay: return {} return {CONF_VERSION: "invalid_version"} def _is_same_device( - gw_type: ConfGatewayType, user_input: Dict[str, str], entry: ConfigEntry + gw_type: ConfGatewayType, user_input: dict[str, str], entry: ConfigEntry ): """Check if another ConfigDevice is actually the same as user_input. @@ -104,9 +104,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up config flow.""" - self._gw_type: Optional[str] = None + self._gw_type: str | None = None - async def async_step_import(self, user_input: Optional[Dict[str, str]] = None): + async def async_step_import(self, user_input: dict[str, str] | None = None): """Import a config entry. This method is called by async_setup and it has already @@ -126,12 +126,12 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL - result: Dict[str, Any] = await self.async_step_user(user_input=user_input) + result: dict[str, Any] = await self.async_step_user(user_input=user_input) if result["type"] == "form": return self.async_abort(reason=next(iter(result["errors"].values()))) return result - async def async_step_user(self, user_input: Optional[Dict[str, str]] = None): + async def async_step_user(self, user_input: dict[str, str] | None = None): """Create a config entry from frontend user input.""" schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} schema = vol.Schema(schema) @@ -148,7 +148,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema) - async def async_step_gw_serial(self, user_input: Optional[Dict[str, str]] = None): + async def async_step_gw_serial(self, user_input: dict[str, str] | None = None): """Create config entry for a serial gateway.""" errors = {} if user_input is not None: @@ -177,7 +177,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="gw_serial", data_schema=schema, errors=errors ) - async def async_step_gw_tcp(self, user_input: Optional[Dict[str, str]] = None): + async def async_step_gw_tcp(self, user_input: dict[str, str] | None = None): """Create a config entry for a tcp gateway.""" errors = {} if user_input is not None: @@ -215,7 +215,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return True return False - async def async_step_gw_mqtt(self, user_input: Optional[Dict[str, str]] = None): + async def async_step_gw_mqtt(self, user_input: dict[str, str] | None = None): """Create a config entry for a mqtt gateway.""" errors = {} if user_input is not None: @@ -271,8 +271,8 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_create_entry( - self, user_input: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, str] | None = None + ) -> dict[str, Any]: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", @@ -285,9 +285,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def validate_common( self, gw_type: ConfGatewayType, - errors: Dict[str, str], - user_input: Optional[Dict[str, str]] = None, - ) -> Dict[str, str]: + errors: dict[str, str], + user_input: dict[str, str] | None = None, + ) -> dict[str, str]: """Validate parameters common to all gateway types.""" if user_input is not None: errors.update(_validate_version(user_input.get(CONF_VERSION))) diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 9116009d7b1..7a9027d9b72 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -1,6 +1,8 @@ """MySensors constants.""" +from __future__ import annotations + from collections import defaultdict -from typing import Dict, List, Literal, Set, Tuple +from typing import Literal, Tuple ATTR_DEVICES: str = "devices" ATTR_GATEWAY_ID: str = "gateway_id" @@ -21,7 +23,7 @@ ConfGatewayType = Literal["Serial", "TCP", "MQTT"] CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial" CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP" CONF_GATEWAY_TYPE_MQTT: ConfGatewayType = "MQTT" -CONF_GATEWAY_TYPE_ALL: List[str] = [ +CONF_GATEWAY_TYPE_ALL: list[str] = [ CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, CONF_GATEWAY_TYPE_TCP, @@ -62,7 +64,7 @@ DevId = Tuple[GatewayId, int, int, int] # The MySensors integration brings these together by creating an entity for every v_type of every child_id of every node. # The DevId tuple perfectly captures this. -BINARY_SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = { +BINARY_SENSOR_TYPES: dict[SensorType, set[ValueType]] = { "S_DOOR": {"V_TRIPPED"}, "S_MOTION": {"V_TRIPPED"}, "S_SMOKE": {"V_TRIPPED"}, @@ -73,23 +75,23 @@ BINARY_SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = { "S_MOISTURE": {"V_TRIPPED"}, } -CLIMATE_TYPES: Dict[SensorType, Set[ValueType]] = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} +CLIMATE_TYPES: dict[SensorType, set[ValueType]] = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} -COVER_TYPES: Dict[SensorType, Set[ValueType]] = { +COVER_TYPES: dict[SensorType, set[ValueType]] = { "S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"} } -DEVICE_TRACKER_TYPES: Dict[SensorType, Set[ValueType]] = {"S_GPS": {"V_POSITION"}} +DEVICE_TRACKER_TYPES: dict[SensorType, set[ValueType]] = {"S_GPS": {"V_POSITION"}} -LIGHT_TYPES: Dict[SensorType, Set[ValueType]] = { +LIGHT_TYPES: dict[SensorType, set[ValueType]] = { "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"}, "S_RGB_LIGHT": {"V_RGB"}, "S_RGBW_LIGHT": {"V_RGBW"}, } -NOTIFY_TYPES: Dict[SensorType, Set[ValueType]] = {"S_INFO": {"V_TEXT"}} +NOTIFY_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}} -SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = { +SENSOR_TYPES: dict[SensorType, set[ValueType]] = { "S_SOUND": {"V_LEVEL"}, "S_VIBRATION": {"V_LEVEL"}, "S_MOISTURE": {"V_LEVEL"}, @@ -117,7 +119,7 @@ SENSOR_TYPES: Dict[SensorType, Set[ValueType]] = { "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"}, } -SWITCH_TYPES: Dict[SensorType, Set[ValueType]] = { +SWITCH_TYPES: dict[SensorType, set[ValueType]] = { "S_LIGHT": {"V_LIGHT"}, "S_BINARY": {"V_STATUS"}, "S_DOOR": {"V_ARMED"}, @@ -134,7 +136,7 @@ SWITCH_TYPES: Dict[SensorType, Set[ValueType]] = { } -PLATFORM_TYPES: Dict[str, Dict[SensorType, Set[ValueType]]] = { +PLATFORM_TYPES: dict[str, dict[SensorType, set[ValueType]]] = { "binary_sensor": BINARY_SENSOR_TYPES, "climate": CLIMATE_TYPES, "cover": COVER_TYPES, @@ -145,19 +147,19 @@ PLATFORM_TYPES: Dict[str, Dict[SensorType, Set[ValueType]]] = { "switch": SWITCH_TYPES, } -FLAT_PLATFORM_TYPES: Dict[Tuple[str, SensorType], Set[ValueType]] = { +FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = { (platform, s_type_name): v_type_name for platform, platform_types in PLATFORM_TYPES.items() for s_type_name, v_type_name in platform_types.items() } -TYPE_TO_PLATFORMS: Dict[SensorType, List[str]] = defaultdict(list) +TYPE_TO_PLATFORMS: dict[SensorType, list[str]] = defaultdict(list) for platform, platform_types in PLATFORM_TYPES.items(): for s_type_name in platform_types: TYPE_TO_PLATFORMS[s_type_name].append(platform) -SUPPORTED_PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - { +PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - { "notify", "device_tracker", } diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 782ab88c488..0e3478a57bf 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,4 +1,5 @@ """Support for MySensors covers.""" +from enum import Enum, unique import logging from typing import Callable @@ -14,6 +15,16 @@ from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) +@unique +class CoverState(Enum): + """An enumeration of the standard cover states.""" + + OPEN = 0 + OPENING = 1 + CLOSING = 2 + CLOSED = 3 + + async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable ): @@ -43,13 +54,44 @@ async def async_setup_entry( class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): """Representation of the value of a MySensors Cover child node.""" + def get_cover_state(self): + """Return a CoverState enum representing the state of the cover.""" + set_req = self.gateway.const.SetReq + v_up = self._values.get(set_req.V_UP) == STATE_ON + v_down = self._values.get(set_req.V_DOWN) == STATE_ON + v_stop = self._values.get(set_req.V_STOP) == STATE_ON + + # If a V_DIMMER or V_PERCENTAGE is available, that is the amount + # the cover is open. Otherwise, use 0 or 100 based on the V_LIGHT + # or V_STATUS. + amount = 100 + if set_req.V_DIMMER in self._values: + amount = self._values.get(set_req.V_DIMMER) + else: + amount = 100 if self._values.get(set_req.V_LIGHT) == STATE_ON else 0 + + if v_up and not v_down and not v_stop: + return CoverState.OPENING + if not v_up and v_down and not v_stop: + return CoverState.CLOSING + if not v_up and not v_down and v_stop and amount == 0: + return CoverState.CLOSED + return CoverState.OPEN + @property def is_closed(self): - """Return True if cover is closed.""" - set_req = self.gateway.const.SetReq - if set_req.V_DIMMER in self._values: - return self._values.get(set_req.V_DIMMER) == 0 - return self._values.get(set_req.V_LIGHT) == STATE_OFF + """Return True if the cover is closed.""" + return self.get_cover_state() == CoverState.CLOSED + + @property + def is_closing(self): + """Return True if the cover is closing.""" + return self.get_cover_state() == CoverState.CLOSING + + @property + def is_opening(self): + """Return True if the cover is opening.""" + return self.get_cover_state() == CoverState.OPENING @property def current_cover_position(self): diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 68414867345..4e770f70bf0 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,7 +1,9 @@ """Handle MySensors devices.""" +from __future__ import annotations + from functools import partial import logging -from typing import Any, Dict, Optional +from typing import Any from mysensors import BaseAsyncGateway, Sensor from mysensors.sensor import ChildSensor @@ -107,7 +109,7 @@ class MySensorsDevice: return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}" @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> dict[str, Any] | None: """Return a dict that allows home assistant to puzzle all entities belonging to a node together.""" return { "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, @@ -122,7 +124,7 @@ class MySensorsDevice: return f"{self.node_name} {self.child_id}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] @@ -162,6 +164,9 @@ class MySensorsDevice: set_req.V_LIGHT, set_req.V_LOCK_STATUS, set_req.V_TRIPPED, + set_req.V_UP, + set_req.V_DOWN, + set_req.V_STOP, ): self._values[value_type] = STATE_ON if int(value) == 1 else STATE_OFF elif value_type == set_req.V_DIMMER: @@ -193,7 +198,7 @@ class MySensorsDevice: self.hass.loop.call_later(UPDATE_DELAY, delayed_update) -def get_mysensors_devices(hass, domain: str) -> Dict[DevId, MySensorsDevice]: +def get_mysensors_devices(hass, domain: str) -> dict[DevId, MySensorsDevice]: """Return MySensors devices for a hass platform name.""" if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]: hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index d1f89e4fe04..068029af960 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -72,5 +72,5 @@ class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): host_name=self.name, gps=(latitude, longitude), battery=node.battery_level, - attributes=self.device_state_attributes, + attributes=self.extra_state_attributes, ) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index a7f3a053d3f..6cf8e7d7383 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -1,10 +1,12 @@ """Handle MySensors gateways.""" +from __future__ import annotations + import asyncio from collections import defaultdict import logging import socket import sys -from typing import Any, Callable, Coroutine, Dict, Optional +from typing import Any, Callable, Coroutine import async_timeout from mysensors import BaseAsyncGateway, Message, Sensor, mysensors @@ -63,7 +65,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: HomeAssistantType, 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 :( @@ -73,7 +75,7 @@ async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bo def on_conn_made(_: BaseAsyncGateway) -> None: gateway_ready.set() - gateway: Optional[BaseAsyncGateway] = await _get_gateway( + gateway: BaseAsyncGateway | None = await _get_gateway( hass, device=user_input[CONF_DEVICE], version=user_input[CONF_VERSION], @@ -110,7 +112,7 @@ async def try_connect(hass: HomeAssistantType, user_input: Dict[str, str]) -> bo def get_mysensors_gateway( hass: HomeAssistantType, gateway_id: GatewayId -) -> Optional[BaseAsyncGateway]: +) -> BaseAsyncGateway | None: """Return the Gateway for a given GatewayId.""" if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {} @@ -120,7 +122,7 @@ def get_mysensors_gateway( async def setup_gateway( hass: HomeAssistantType, entry: ConfigEntry -) -> Optional[BaseAsyncGateway]: +) -> BaseAsyncGateway | None: """Set up the Gateway for the given ConfigEntry.""" ready_gateway = await _get_gateway( @@ -145,14 +147,14 @@ async def _get_gateway( device: str, version: str, event_callback: Callable[[Message], None], - persistence_file: Optional[str] = None, - baud_rate: Optional[int] = None, - tcp_port: Optional[int] = None, - topic_in_prefix: Optional[str] = None, - topic_out_prefix: Optional[str] = None, + persistence_file: str | None = None, + baud_rate: int | None = None, + tcp_port: int | None = None, + topic_in_prefix: str | None = None, + topic_out_prefix: str | None = None, retain: bool = False, persistence: bool = True, # old persistence option has been deprecated. kwarg is here so we can run try_connect() without persistence -) -> Optional[BaseAsyncGateway]: +) -> BaseAsyncGateway | None: """Return gateway after setup of the gateway.""" if persistence_file is not None: diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index a47c9174b23..d21140701f9 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -1,5 +1,5 @@ """Handle MySensors messages.""" -from typing import Dict, List +from __future__ import annotations from mysensors import Message @@ -70,16 +70,16 @@ async def handle_sketch_version( @callback def _handle_child_update( - hass: HomeAssistantType, gateway_id: GatewayId, validated: Dict[str, List[DevId]] + hass: HomeAssistantType, gateway_id: GatewayId, validated: dict[str, list[DevId]] ): """Handle a child update.""" - signals: List[str] = [] + signals: list[str] = [] # Update all platforms for the device via dispatcher. # Add/update entity for validated children. for platform, dev_ids in validated.items(): devices = get_mysensors_devices(hass, platform) - new_dev_ids: List[DevId] = [] + new_dev_ids: list[DevId] = [] for dev_id in dev_ids: if dev_id in devices: signals.append(CHILD_CALLBACK.format(*dev_id)) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 0b8dc361158..0d18b243520 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -1,8 +1,10 @@ """Helper functions for mysensors package.""" +from __future__ import annotations + from collections import defaultdict from enum import IntEnum import logging -from typing import Callable, DefaultDict, Dict, List, Optional, Set, Union +from typing import Callable, DefaultDict from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor @@ -35,7 +37,7 @@ SCHEMAS = Registry() async def on_unload( - hass: HomeAssistantType, entry: Union[ConfigEntry, GatewayId], fnct: Callable + hass: HomeAssistantType, entry: ConfigEntry | GatewayId, fnct: Callable ) -> None: """Register a callback to be called when entry is unloaded. @@ -53,7 +55,7 @@ async def on_unload( @callback def discover_mysensors_platform( - hass: HomeAssistant, gateway_id: GatewayId, platform: str, new_devices: List[DevId] + hass: HomeAssistant, gateway_id: GatewayId, platform: str, new_devices: list[DevId] ) -> None: """Discover a MySensors platform.""" _LOGGER.debug("Discovering platform %s with devIds: %s", platform, new_devices) @@ -150,7 +152,7 @@ def invalid_msg( ) -def validate_set_msg(gateway_id: GatewayId, msg: Message) -> Dict[str, List[DevId]]: +def validate_set_msg(gateway_id: GatewayId, msg: Message) -> dict[str, list[DevId]]: """Validate a set message.""" if not validate_node(msg.gateway, msg.node_id): return {} @@ -171,34 +173,34 @@ def validate_child( gateway: BaseAsyncGateway, node_id: int, child: ChildSensor, - value_type: Optional[int] = None, -) -> DefaultDict[str, List[DevId]]: + value_type: int | None = None, +) -> 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: Optional[SensorType] = next( + child_type_name: SensorType | None = next( (member.name for member in pres if member.value == child.type), None ) - value_types: Set[int] = {value_type} if value_type else {*child.values} - value_type_names: Set[ValueType] = { + value_types: set[int] = {value_type} if value_type else {*child.values} + value_type_names: set[ValueType] = { member.name for member in set_req if member.value in value_types } - platforms: List[str] = TYPE_TO_PLATFORMS.get(child_type_name, []) + platforms: list[str] = TYPE_TO_PLATFORMS.get(child_type_name, []) if not platforms: _LOGGER.warning("Child type %s is not supported", child.type) return validated for platform in platforms: - platform_v_names: Set[ValueType] = FLAT_PLATFORM_TYPES[ + platform_v_names: set[ValueType] = FLAT_PLATFORM_TYPES[ platform, child_type_name ] - v_names: Set[ValueType] = platform_v_names & value_type_names + v_names: set[ValueType] = platform_v_names & value_type_names if not v_names: - child_value_names: Set[ValueType] = { + child_value_names: set[ValueType] = { member.name for member in set_req if member.value in child.values } - v_names: Set[ValueType] = platform_v_names & child_value_names + v_names: set[ValueType] = platform_v_names & child_value_names for v_name in v_names: child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a09f8af1394..a62318aea53 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -4,7 +4,7 @@ from typing import Callable from homeassistant.components import mysensors from homeassistant.components.mysensors import on_unload from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONDUCTIVITY, @@ -87,7 +87,7 @@ async def async_setup_entry( ) -class MySensorsSensor(mysensors.device.MySensorsEntity): +class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity): """Representation of a MySensors Sensor child node.""" @property diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json new file mode 100644 index 00000000000..854e88b38b9 --- /dev/null +++ b/homeassistant/components/mysensors/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "gw_serial": { + "data": { + "device": "\u0421\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442" + } + }, + "gw_tcp": { + "data": { + "tcp_port": "\u043f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 189226f29d5..d05e2bdb47b 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -4,13 +4,55 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_device": "Ung\u00fcltiges Ger\u00e4t", + "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_serial": "Ung\u00fcltiger Serieller Port", + "invalid_version": "Ung\u00fcltige MySensors Version", + "not_a_number": "Bitte eine Nummer eingeben", "unknown": "Unerwarteter Fehler" }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_device": "Ung\u00fcltiges Ger\u00e4t", + "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_serial": "Ung\u00fcltiger Serieller Port", + "invalid_version": "Ung\u00fcltige MySensors Version", + "not_a_number": "Bitte eine Nummer eingeben", "unknown": "Unerwarteter Fehler" + }, + "step": { + "gw_mqtt": { + "data": { + "retain": "MQTT behalten", + "version": "MySensors Version" + }, + "description": "MQTT-Gateway einrichten" + }, + "gw_serial": { + "data": { + "baud_rate": "Baudrate", + "device": "Serielle Schnittstelle", + "version": "MySensors Version" + }, + "description": "Einrichtung des seriellen Gateways" + }, + "gw_tcp": { + "data": { + "device": "IP-Adresse des Gateways", + "tcp_port": "Port", + "version": "MySensors Version" + }, + "description": "Einrichtung des Ethernet-Gateways" + }, + "user": { + "data": { + "gateway_type": "Gateway-Typ" + }, + "description": "Verbindungsmethode zum Gateway w\u00e4hlen" + } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json new file mode 100644 index 00000000000..7d4df1f12da --- /dev/null +++ b/homeassistant/components/mysensors/translations/hu.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_serial": "\u00c9rv\u00e9nytelen soros port", + "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", + "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "gw_mqtt": { + "data": { + "version": "MySensors verzi\u00f3" + } + }, + "gw_serial": { + "data": { + "version": "MySensors verzi\u00f3" + } + }, + "gw_tcp": { + "data": { + "tcp_port": "port", + "version": "MySensors verzi\u00f3" + } + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json new file mode 100644 index 00000000000..e982250b09c --- /dev/null +++ b/homeassistant/components/mysensors/translations/id.json @@ -0,0 +1,75 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "duplicate_persistence_file": "File persistensi sudah digunakan", + "duplicate_topic": "Topik sudah digunakan", + "invalid_auth": "Autentikasi tidak valid", + "invalid_device": "Perangkat tidak valid", + "invalid_ip": "Alamat IP tidak valid", + "invalid_persistence_file": "File persistensi tidak valid", + "invalid_port": "Nomor port tidak valid", + "invalid_serial": "Port serial tidak valid", + "invalid_subscribe_topic": "Topik langganan tidak valid", + "invalid_version": "Versi MySensors tidak valid", + "not_a_number": "Masukkan angka", + "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "duplicate_persistence_file": "File persistensi sudah digunakan", + "duplicate_topic": "Topik sudah digunakan", + "invalid_auth": "Autentikasi tidak valid", + "invalid_device": "Perangkat tidak valid", + "invalid_ip": "Alamat IP tidak valid", + "invalid_persistence_file": "File persistensi tidak valid", + "invalid_port": "Nomor port tidak valid", + "invalid_serial": "Port serial tidak valid", + "invalid_subscribe_topic": "Topik langganan tidak valid", + "invalid_version": "Versi MySensors tidak valid", + "not_a_number": "Masukkan angka", + "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "file persistensi (kosongkan untuk dihasilkan otomatis)", + "retain": "mqtt retain", + "topic_in_prefix": "prefiks untuk topik input (topic_in_prefix)", + "topic_out_prefix": "prefiks untuk topik output (topic_out_prefix)", + "version": "Versi MySensors" + }, + "description": "Penyiapan gateway MQTT" + }, + "gw_serial": { + "data": { + "baud_rate": "tingkat baud", + "device": "Port serial", + "persistence_file": "file persistensi (kosongkan untuk dihasilkan otomatis)", + "version": "Versi MySensors" + }, + "description": "Penyiapan gateway serial" + }, + "gw_tcp": { + "data": { + "device": "Alamat IP gateway", + "persistence_file": "file persistensi (kosongkan untuk dihasilkan otomatis)", + "tcp_port": "port", + "version": "Versi MySensors" + }, + "description": "Pengaturan gateway Ethernet" + }, + "user": { + "data": { + "gateway_type": "Jenis gateway" + }, + "description": "Pilih metode koneksi ke gateway" + } + } + }, + "title": "MySensors" +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ko.json b/homeassistant/components/mysensors/translations/ko.json index bb38f94bc92..e57f60aafbf 100644 --- a/homeassistant/components/mysensors/translations/ko.json +++ b/homeassistant/components/mysensors/translations/ko.json @@ -3,14 +3,77 @@ "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", + "duplicate_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4", + "duplicate_topic": "\ud1a0\ud53d\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_device": "\uae30\uae30\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_port": "\ud3ec\ud2b8 \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_publish_topic": "\ubc1c\ud589 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_serial": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_subscribe_topic": "\uad6c\ub3c5 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_version": "MySensors \ubc84\uc804\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_a_number": "\uc22b\uc790\ub85c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "port_out_of_range": "\ud3ec\ud2b8 \ubc88\ud638\ub294 1 \uc774\uc0c1 65535 \uc774\ud558\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", + "same_topic": "\uad6c\ub3c5 \ubc0f \ubc1c\ud589 \ud1a0\ud53d\uc740 \ub3d9\uc77c\ud569\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "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", + "duplicate_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4", + "duplicate_topic": "\ud1a0\ud53d\uc774 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_device": "\uae30\uae30\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_port": "\ud3ec\ud2b8 \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_publish_topic": "\ubc1c\ud589 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_serial": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_subscribe_topic": "\uad6c\ub3c5 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_version": "MySensors \ubc84\uc804\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_a_number": "\uc22b\uc790\ub85c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "port_out_of_range": "\ud3ec\ud2b8 \ubc88\ud638\ub294 1 \uc774\uc0c1 65535 \uc774\ud558\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", + "same_topic": "\uad6c\ub3c5 \ubc0f \ubc1c\ud589 \ud1a0\ud53d\uc740 \ub3d9\uc77c\ud569\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c(\uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694)", + "retain": "mqtt retain", + "topic_in_prefix": "\uc785\ub825 \ud1a0\ud53d\uc758 \uc811\ub450\uc0ac (topic_in_prefix)", + "topic_out_prefix": "\ucd9c\ub825 \ud1a0\ud53d\uc758 \uc811\ub450\uc0ac (topic_out_prefix)", + "version": "MySensors \ubc84\uc804" + }, + "description": "MQTT \uac8c\uc774\ud2b8\uc6e8\uc774 \uc124\uc815" + }, + "gw_serial": { + "data": { + "baud_rate": "\uc804\uc1a1 \uc18d\ub3c4", + "device": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8", + "persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c(\uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694)", + "version": "MySensors \ubc84\uc804" + }, + "description": "\uc2dc\ub9ac\uc5bc \uac8c\uc774\ud2b8\uc6e8\uc774 \uc124\uc815" + }, + "gw_tcp": { + "data": { + "device": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc758 IP \uc8fc\uc18c", + "persistence_file": "\uc9c0\uc18d\uc131 \ud30c\uc77c(\uc790\ub3d9\uc73c\ub85c \uc0dd\uc131\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694)", + "tcp_port": "\ud3ec\ud2b8", + "version": "MySensors \ubc84\uc804" + }, + "description": "\uc774\ub354\ub137 \uac8c\uc774\ud2b8\uc6e8\uc774 \uc124\uc815" + }, + "user": { + "data": { + "gateway_type": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc720\ud615" + }, + "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc5f0\uacb0 \ubc29\ubc95\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" + } } - } + }, + "title": "MySensors" } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index e41f67c7730..49ddf987cef 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -3,13 +3,16 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", + "duplicate_persistence_file": "Persistentiebestand al in gebruik", "duplicate_topic": "Topic is al in gebruik", "invalid_auth": "Ongeldige authenticatie", "invalid_device": "Ongeldig apparaat", "invalid_ip": "Ongeldig IP-adres", + "invalid_persistence_file": "Ongeldig persistentiebestand", "invalid_port": "Ongeldig poortnummer", "invalid_publish_topic": "Ongeldig publiceer topic", "invalid_serial": "Ongeldige seri\u00eble poort", + "invalid_subscribe_topic": "Ongeldig abonneeronderwerp", "invalid_version": "Ongeldige MySensors-versie", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index 510245ea859..145bdadcc2a 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -145,7 +145,7 @@ class MyStromLight(LightEntity): try: await self._bulb.set_off() except MyStromConnectionError: - _LOGGER.warning("myStrom bulb not online") + _LOGGER.warning("The myStrom bulb not online") async def async_update(self): """Fetch new state data for this light.""" diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py index f8379cb310f..b1e83cd5311 100644 --- a/homeassistant/components/n26/__init__.py +++ b/homeassistant/components/n26/__init__.py @@ -36,7 +36,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -N26_COMPONENTS = ["sensor", "switch"] +PLATFORMS = ["sensor", "switch"] def setup(hass, config): @@ -65,9 +65,9 @@ def setup(hass, config): hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA] = api_data_list - # Load components for supported devices - for component in N26_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + # Load platforms for supported devices + for platform in PLATFORMS: + load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/n26/sensor.py b/homeassistant/components/n26/sensor.py index b9a8b21f9d0..98d86194b86 100644 --- a/homeassistant/components/n26/sensor.py +++ b/homeassistant/components/n26/sensor.py @@ -1,5 +1,5 @@ """Support for N26 bank account sensors.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from . import DEFAULT_SCAN_INTERVAL, DOMAIN, timestamp_ms_to_date from .const import DATA @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensor_entities) -class N26Account(Entity): +class N26Account(SensorEntity): """Sensor for a N26 balance account. A balance account contains an amount of money (=balance). The amount may @@ -86,7 +86,7 @@ class N26Account(Entity): return self._data.balance.get("currency") @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Additional attributes of the sensor.""" attributes = { ATTR_IBAN: self._data.balance.get("iban"), @@ -117,7 +117,7 @@ class N26Account(Entity): return ICON_ACCOUNT -class N26Card(Entity): +class N26Card(SensorEntity): """Sensor for a N26 card.""" def __init__(self, api_data, card) -> None: @@ -147,7 +147,7 @@ class N26Card(Entity): return self._card["status"] @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Additional attributes of the sensor.""" attributes = { "apple_pay_eligible": self._card.get("applePayEligible"), @@ -186,7 +186,7 @@ class N26Card(Entity): return ICON_CARD -class N26Space(Entity): +class N26Space(SensorEntity): """Sensor for a N26 space.""" def __init__(self, api_data, space) -> None: @@ -220,7 +220,7 @@ class N26Space(Entity): return self._space["balance"]["currency"] @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Additional attributes of the sensor.""" goal_value = "" if "goal" in self._space: diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 782b8735e3a..e7f83c66efa 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -163,7 +163,7 @@ class NAD(MediaPlayerEntity): @property def source_list(self): """List of available input sources.""" - return sorted(list(self._reverse_mapping)) + return sorted(self._reverse_mapping) @property def available(self): diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index ed1e4877a31..a6f453ce2aa 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -62,14 +62,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): token = "" if discovery_info is not None: host = discovery_info["host"] - name = discovery_info["hostname"] + name = None + device_id = discovery_info["properties"]["id"] + # if device already exists via config, skip discovery setup if host in hass.data[DATA_NANOLEAF]: return _LOGGER.info("Discovered a new Nanoleaf: %s", discovery_info) conf = load_json(hass.config.path(CONFIG_FILE)) - if conf.get(host, {}).get("token"): - token = conf[host]["token"] + if host in conf and device_id not in conf: + conf[device_id] = conf.pop(host) + save_json(hass.config.path(CONFIG_FILE), conf) + token = conf.get(device_id, {}).get("token", "") else: host = config[CONF_HOST] name = config[CONF_NAME] @@ -94,11 +98,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): nanoleaf_light.token = token try: - nanoleaf_light.available + info = nanoleaf_light.info except Unavailable: _LOGGER.error("Could not connect to Nanoleaf Light: %s on %s", name, host) return + if name is None: + name = info.name + hass.data[DATA_NANOLEAF][host] = nanoleaf_light add_entities([NanoleafLight(nanoleaf_light, name)], True) @@ -108,6 +115,7 @@ class NanoleafLight(LightEntity): def __init__(self, light, name): """Initialize an Nanoleaf light.""" + self._unique_id = light.serialNo self._available = True self._brightness = None self._color_temp = None @@ -157,6 +165,11 @@ class NanoleafLight(LightEntity): """Return the warmest color_temp that this light supports.""" return 833 + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def name(self): """Return the display name of this light.""" diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 6d953335a34..1f0fbf80983 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -2,6 +2,6 @@ "domain": "nanoleaf", "name": "Nanoleaf", "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["pynanoleaf==0.0.5"], + "requirements": ["pynanoleaf==0.1.0"], "codeowners": [] } diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 1d9d3de4f89..bb0db8ebd85 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -103,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[NEATO_LOGIN] = hub - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 1698a1d944a..9a2f47bcfa3 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -102,7 +102,7 @@ class NeatoCleaningMap(Camera): self._image = image.read() self._image_url = image_url - self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") + self._generated_at = map_data["generated_at"] self._available = True @property @@ -126,7 +126,7 @@ class NeatoCleaningMap(Camera): return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the vacuum cleaner.""" data = {} diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 1f2f575ae50..3f7f7831f54 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Neato Botvac.""" +from __future__ import annotations + import logging -from typing import Optional import voluptuous as vol @@ -8,7 +9,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow -# pylint: disable=unused-import from .const import NEATO_DOMAIN @@ -25,7 +25,7 @@ class OAuth2FlowHandler( """Return logger.""" return logging.getLogger(__name__) - async def async_step_user(self, user_input: Optional[dict] = None) -> dict: + async def async_step_user(self, user_input: dict | None = None) -> dict: """Create an entry for the flow.""" current_entries = self._async_current_entries() if current_entries and CONF_TOKEN in current_entries[0].data: @@ -38,9 +38,7 @@ class OAuth2FlowHandler( """Perform reauth upon migration of old entries.""" return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm( - self, user_input: Optional[dict] = None - ) -> dict: + async def async_step_reauth_confirm(self, user_input: dict | None = None) -> dict: """Confirm reauth upon migration of old entries.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 248e455b6da..0cd8fb932ce 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -119,6 +119,7 @@ ERRORS = { "nav_backdrop_frontbump": "Clear my path", "nav_backdrop_leftbump": "Clear my path", "nav_backdrop_wheelextended": "Clear my path", + "nav_floorplan_zone_path_blocked": "Clear my path", "nav_mag_sensor": "Clear my path", "nav_no_exit": "Clear my path", "nav_no_movement": "Clear my path", diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 50af42e7007..83add4ff3f7 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -4,9 +4,8 @@ import logging from pybotvac.exceptions import NeatoRobotException -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity from homeassistant.const import PERCENTAGE -from homeassistant.helpers.entity import Entity from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES @@ -31,7 +30,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(dev, True) -class NeatoSensor(Entity): +class NeatoSensor(SensorEntity): """Neato sensor.""" def __init__(self, neato, robot): diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 4c2fc456873..272191a4221 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -20,7 +20,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "title": "M\u00f6chtest du mit der Einrichtung beginnen?" + "title": "M\u00f6chten Sie mit der Einrichtung beginnen?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/he.json b/homeassistant/components/neato/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/neato/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index f2fd30f323e..3cb6ffd3364 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -1,15 +1,26 @@ { "config": { "abort": { - "already_configured": "M\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "create_entry": { - "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )." + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + }, "reauth_confirm": { - "title": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?" + "title": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" }, "user": { "data": { @@ -17,9 +28,10 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "vendor": "Sz\u00e1ll\u00edt\u00f3" }, - "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} ).", + "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3]({docs_url}).", "title": "Neato Fi\u00f3kinform\u00e1ci\u00f3" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/id.json b/homeassistant/components/neato/translations/id.json new file mode 100644 index 00000000000..17eee515787 --- /dev/null +++ b/homeassistant/components/neato/translations/id.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "invalid_auth": "Autentikasi tidak valid", + "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})", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "title": "Ingin memulai penyiapan?" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna", + "vendor": "Vendor" + }, + "description": "Baca [dokumentasi Neato]({docs_url}).", + "title": "Info Akun Neato" + } + } + }, + "title": "Neato Botvac" +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ko.json b/homeassistant/components/neato/translations/ko.json index 359aeefcc78..d08000871ea 100644 --- a/homeassistant/components/neato/translations/ko.json +++ b/homeassistant/components/neato/translations/ko.json @@ -32,5 +32,6 @@ "title": "Neato \uacc4\uc815 \uc815\ubcf4" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index 563e6500c16..d03bc1d216a 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Al geconfigureerd", + "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.", @@ -9,7 +9,7 @@ "reauth_successful": "Herauthenticatie was succesvol" }, "create_entry": { - "default": "Zie [Neato-documentatie] ({docs_url})." + "default": "Succesvol geauthenticeerd" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index ea1be16d7ac..25bb616a638 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -25,7 +25,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index ce4156244b7..e0b3c7b779f 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -202,8 +202,8 @@ class NeatoConnectedVacuum(StateVacuumEntity): return mapdata = self._mapdata[self._robot_serial]["maps"][0] - self._clean_time_start = (mapdata["start_at"].strip("Z")).replace("T", " ") - self._clean_time_stop = (mapdata["end_at"].strip("Z")).replace("T", " ") + self._clean_time_start = mapdata["start_at"] + self._clean_time_stop = mapdata["end_at"] self._clean_area = mapdata["cleaned_area"] self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"] self._clean_susp_time = mapdata["time_in_suspended_cleaning"] @@ -284,7 +284,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): return self._robot_serial @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the vacuum cleaner.""" data = {} diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 3d15e3c4d9b..de8a85f44fd 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -7,11 +7,10 @@ from ns_api import RequestParametersError import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -94,7 +93,7 @@ def valid_stations(stations, given_stations): return True -class NSDepartureSensor(Entity): +class NSDepartureSensor(SensorEntity): """Implementation of a NS Departure Sensor.""" def __init__(self, nsapi, name, departure, heading, via, time): @@ -124,7 +123,7 @@ class NSDepartureSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not self._trips: return diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 61241660847..93e63b05da9 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -48,7 +48,7 @@ class NelloLock(LockEntity): return True @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" return self._device_attrs diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b0abd24012a..cd3f6ed9ed3 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -198,9 +198,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -217,8 +217,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index cc2730fad8a..ce6ff897a2f 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -1,8 +1,8 @@ """Support for Google Nest SDM Cameras.""" +from __future__ import annotations import datetime import logging -from typing import Optional from google_nest_sdm.camera_traits import ( CameraEventImageTrait, @@ -74,7 +74,7 @@ class NestCamera(Camera): return False @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" # The API "name" field is a unique device identifier. return f"{self._device.name}-camera" @@ -145,6 +145,9 @@ class NestCamera(Camera): _LOGGER.debug("Failed to extend stream: %s", err) # Next attempt to catch a url will get a new one self._stream = None + if self.stream: + self.stream.stop() + self.stream = None return # Update the stream worker with the latest valid url if self.stream: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index b0c64329ffd..e02ebcd2dee 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -1,6 +1,5 @@ """Support for Google Nest SDM climate devices.""" - -from typing import Optional +from __future__ import annotations from google_nest_sdm.device import Device from google_nest_sdm.device_traits import FanTrait, TemperatureTrait @@ -111,7 +110,7 @@ class ThermostatEntity(ClimateEntity): return False @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" # The API "name" field is a unique device identifier. return self._device.name @@ -181,9 +180,11 @@ class ThermostatEntity(ClimateEntity): @property def _target_temperature_trait(self): """Return the correct trait with a target temp depending on mode.""" - if self.preset_mode == PRESET_ECO: - if ThermostatEcoTrait.NAME in self._device.traits: - return self._device.traits[ThermostatEcoTrait.NAME] + if ( + self.preset_mode == PRESET_ECO + and ThermostatEcoTrait.NAME in self._device.traits + ): + return self._device.traits[ThermostatEcoTrait.NAME] if ThermostatTemperatureSetpointTrait.NAME in self._device.traits: return self._device.traits[ThermostatTemperatureSetpointTrait.NAME] return None diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 36b0da239a9..fd5eef34c7d 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -11,12 +11,12 @@ and everything else custom is for the old api. When configured with the new api via NestFlowHandler.register_sdm_api, the custom methods just invoke the AbstractOAuth2FlowHandler methods. """ +from __future__ import annotations import asyncio from collections import OrderedDict import logging import os -from typing import Dict import async_timeout import voluptuous as vol @@ -98,7 +98,7 @@ class NestFlowHandler( return logging.getLogger(__name__) @property - def extra_authorize_data(self) -> Dict[str, str]: + def extra_authorize_data(self) -> dict[str, str]: """Extra data that needs to be appended to the authorize url.""" return { "scope": " ".join(SDM_SCOPES), diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index ee16ca1166f..d59ec05c503 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Nest.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -39,7 +39,7 @@ async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str: async def async_get_device_trigger_types( hass: HomeAssistant, nest_device_id: str -) -> List[str]: +) -> list[str]: """List event triggers supported for a Nest device.""" # All devices should have already been loaded so any failures here are # "shouldn't happen" cases @@ -58,7 +58,7 @@ async def async_get_device_trigger_types( return trigger_types -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for a Nest device.""" nest_device_id = await async_get_nest_device_id(hass, device_id) if not nest_device_id: @@ -82,7 +82,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) event_config = event_trigger.TRIGGER_SCHEMA( { event_trigger.CONF_PLATFORM: "event", diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 218b01fd71b..60faa90e8b4 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -28,6 +28,8 @@ from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) +PLATFORMS = ["climate", "camera", "sensor", "binary_sensor"] + # Configuration for the legacy nest API SERVICE_CANCEL_ETA = "cancel_eta" SERVICE_SET_ETA = "set_eta" @@ -131,9 +133,9 @@ async def async_setup_legacy_entry(hass, entry): if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): return False - for component in "climate", "camera", "sensor", "binary_sensor": + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) def validate_structures(target_structures): @@ -361,11 +363,6 @@ class NestSensorDevice(Entity): """Return the name of the nest, if any.""" return self._name - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - @property def should_poll(self): """Do not need poll thanks using Nest streaming API.""" diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 34f525ca7a6..53d9c824466 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -1,6 +1,7 @@ """Support for Nest Thermostat sensors for the legacy API.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_SENSORS, @@ -149,9 +150,14 @@ async def async_setup_legacy_entry(hass, entry, async_add_entities): async_add_entities(await hass.async_add_executor_job(get_sensors), True) -class NestBasicSensor(NestSensorDevice): +class NestBasicSensor(NestSensorDevice, SensorEntity): """Representation a basic Nest sensor.""" + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + @property def state(self): """Return the state of the sensor.""" @@ -179,7 +185,7 @@ class NestBasicSensor(NestSensorDevice): self._state = getattr(self.device, self.variable) -class NestTempSensor(NestSensorDevice): +class NestTempSensor(NestSensorDevice, SensorEntity): """Representation of a Nest Temperature sensor.""" @property @@ -187,6 +193,11 @@ class NestTempSensor(NestSensorDevice): """Return the state of the sensor.""" return self._state + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 52490f41f86..06e2b68d7cf 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -1,12 +1,13 @@ """Support for Google Nest SDM sensors.""" +from __future__ import annotations import logging -from typing import Optional from google_nest_sdm.device import Device from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait from google_nest_sdm.exceptions import GoogleNestException +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, @@ -15,7 +16,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN @@ -53,7 +53,7 @@ async def async_setup_sdm_entry( async_add_entities(entities) -class SensorBase(Entity): +class SensorBase(SensorEntity): """Representation of a dynamically updated Sensor.""" def __init__(self, device: Device): @@ -67,7 +67,7 @@ class SensorBase(Entity): return False @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" # The API "name" field is a unique device identifier. return f"{self._device.name}-{self.device_class}" @@ -113,7 +113,7 @@ class HumiditySensor(SensorBase): """Representation of a Humidity Sensor.""" @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" # The API returns the identifier under the name field. return f"{self._device.name}-humidity" diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 6ce529621aa..26ec49c0d75 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -17,7 +17,7 @@ }, "link": { "title": "Link Nest Account", - "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.", + "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided PIN code below.", "data": { "code": "[%key:common::config_flow::data::pin%]" } diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 08d8cb97454..bc19d5c2c7c 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -30,7 +30,7 @@ "data": { "code": "Codi PIN" }, - "description": "Per enlla\u00e7ar el teu compte de Nest, [autoritza el teu compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa el codi pin que es mostra a sota.", + "description": "Per enlla\u00e7ar el teu compte de Nest, [autoritza el teu compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa el codi PIN que es mostra a sota.", "title": "Enlla\u00e7 amb el compte de Nest" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index f45c9ea489f..d7b000d921f 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -30,7 +30,7 @@ "data": { "code": "PIN Code" }, - "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.", + "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided PIN code below.", "title": "Link Nest Account" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 7d22dfd96bf..1319bc2ca4b 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -30,7 +30,7 @@ "data": { "code": "PIN kood" }, - "description": "Nest-i konto linkimiseks [authorize your account] ({url}).\n\nP\u00e4rast autoriseerimist kopeeri allolev PIN kood.", + "description": "Nest-i konto linkimiseks [authorize your account] ({url}).\n\nP\u00e4rast autoriseerimist kopeeri ja kleebi allolev PIN kood.", "title": "Lingi Nesti konto" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 2830bf5da87..ce716fb3083 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -30,7 +30,7 @@ "data": { "code": "Code PIN" }, - "description": "Pour associer votre compte Nest, [autorisez votre compte]({url}). \n\n Apr\u00e8s autorisation, copiez-collez le code PIN fourni ci-dessous.", + "description": "Pour associer votre compte Nest, [autorisez votre compte]({url}). \n\n Apr\u00e8s autorisation, copiez-collez le code NIP fourni ci-dessous.", "title": "Lier un compte Nest" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 47334c4aa62..9400ea7875c 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -3,34 +3,42 @@ "abort": { "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, "create_entry": { - "default": "Sikeres autentik\u00e1ci\u00f3" + "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { "internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l", - "invalid_pin": "\u00c9rv\u00e9nytelen ", + "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", - "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "init": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, - "description": "V\u00e1laszd ki, hogy melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n\u00e1l szeretn\u00e9d hiteles\u00edteni a Nestet.", + "description": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert", "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" }, "link": { "data": { "code": "PIN-k\u00f3d" }, - "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t] ( {url} ). \n\n Az enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az al\u00e1bbi PIN k\u00f3dot.", + "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t]({url}). \n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az PIN k\u00f3dot.", "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" }, + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + }, "reauth_confirm": { - "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" + "description": "A Nest integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kodat", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } }, diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 4fc229c0d53..757c53e2866 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -2,28 +2,52 @@ "config": { "abort": { "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", - "authorize_url_timeout": "Waktu tunggu menghasilkan otorisasi url telah habis." + "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})", + "reauth_successful": "Autentikasi ulang berhasil", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi." + }, + "create_entry": { + "default": "Berhasil diautentikasi" }, "error": { - "internal_error": "Kesalahan Internal memvalidasi kode", - "timeout": "Waktu tunggu memvalidasi kode telah habis.", - "unknown": "Error tidak diketahui saat memvalidasi kode" + "internal_error": "Kesalahan internal saat memvalidasi kode", + "invalid_pin": "Invalid Kode PIN", + "timeout": "Tenggang waktu memvalidasi kode telah habis.", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "init": { "data": { "flow_impl": "Penyedia" }, - "description": "Pilih melalui penyedia autentikasi mana yang ingin Anda autentikasi dengan Nest.", - "title": "Penyedia Otentikasi" + "description": "Pilih Metode Autentikasi", + "title": "Penyedia Autentikasi" }, "link": { "data": { "code": "Kode PIN" }, - "description": "Untuk menautkan akun Nest Anda, [beri kuasa akun Anda] ( {url} ). \n\n Setelah otorisasi, salin-tempel kode pin yang disediakan di bawah ini.", - "title": "Hubungkan Akun Nest" + "description": "Untuk menautkan akun Nest Anda, [otorisasi akun Anda]({url}).\n\nSetelah otorisasi, salin dan tempel kode PIN yang disediakan di bawah ini.", + "title": "Tautkan Akun Nest" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Nest perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Gerakan terdeteksi", + "camera_person": "Orang terdeteksi", + "camera_sound": "Suara terdeteksi", + "doorbell_chime": "Bel pintu ditekan" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 84c04049946..c6e62db314d 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -30,7 +30,7 @@ "data": { "code": "Codice PIN" }, - "description": "Per collegare l'account Nido, [autorizzare l'account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito di seguito.", + "description": "Per collegare il tuo account Nest, [autorizza il tuo account] ({url}). \n\nDopo l'autorizzazione, copia e incolla il codice PIN fornito di seguito.", "title": "Collega un account Nest" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index f5a0fcf39d1..f8d6de2244a 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "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.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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.", "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." }, "create_entry": { @@ -30,15 +30,24 @@ "data": { "code": "PIN \ucf54\ub4dc" }, - "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url}) \uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 PIN \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", + "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74 [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4 \uc544\ub798\uc5d0 \uc81c\uacf5\ub41c PIN \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec \ub123\uc5b4\uc8fc\uc138\uc694.", "title": "Nest \uacc4\uc815 \uc5f0\uacb0\ud558\uae30" }, "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, "reauth_confirm": { - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + "description": "Nest \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "camera_person": "\uc0ac\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "camera_sound": "\uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc744 \ub54c", + "doorbell_chime": "\ucd08\uc778\uc885\uc774 \ub20c\ub838\uc744 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 387b1effcb0..b4a965f4955 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", - "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De Netatmo-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", @@ -16,19 +16,19 @@ "internal_error": "Interne foutvalidatiecode", "invalid_pin": "Ongeldige PIN-code", "timeout": "Time-out validatie van code", - "unknown": "Onbekende foutvalidatiecode" + "unknown": "Onverwachte fout" }, "step": { "init": { "data": { "flow_impl": "Leverancier" }, - "description": "Kies met welke authenticatieleverancier u wilt verifi\u00ebren met Nest.", + "description": "Kies een authenticatie methode", "title": "Authenticatieleverancier" }, "link": { "data": { - "code": "Pincode" + "code": "PIN-code" }, "description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.", "title": "Koppel Nest-account" diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 14166f7d9a6..d6b6c89bcaa 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -30,7 +30,7 @@ "data": { "code": "PIN kode" }, - "description": "For \u00e5 koble din Nest-konto m\u00e5 du [bekrefte kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", + "description": "For \u00e5 koble til Nest-kontoen din, [autoriser kontoen din] ( {url} ). \n\n Etter autorisasjon, kopier og lim inn den angitte PIN-koden nedenfor.", "title": "Koble til Nest konto" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 4f2e8952566..07ac5246cbf 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -30,7 +30,7 @@ "data": { "code": "PIN-\u043a\u043e\u0434" }, - "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", + "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 PIN-\u043a\u043e\u0434.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 0abe4d75fac..cddb9e2fe79 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -25,5 +25,10 @@ "title": "L\u00e4nka Nest-konto" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "R\u00f6relse uppt\u00e4ckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index cdbd34991f2..b9b04a08feb 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -43,7 +43,7 @@ from .const import ( OAUTH2_TOKEN, ) from .data_handler import NetatmoDataHandler -from .webhook import handle_webhook +from .webhook import async_handle_webhook _LOGGER = logging.getLogger(__name__) @@ -111,9 +111,9 @@ 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 component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) async def unregister_webhook(_): @@ -157,18 +157,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: webhook_register( - hass, DOMAIN, "Netatmo", entry.data[CONF_WEBHOOK_ID], handle_webhook + hass, + DOMAIN, + "Netatmo", + entry.data[CONF_WEBHOOK_ID], + async_handle_webhook, ) async def handle_event(event): """Handle webhook events.""" if event["data"]["push_type"] == "webhook_activation": if activation_listener is not None: - _LOGGER.debug("sub called") activation_listener() if activation_timeout is not None: - _LOGGER.debug("Unsub called") activation_timeout() activation_listener = async_dispatcher_connect( @@ -205,15 +207,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): await hass.async_add_executor_job( hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook ) - _LOGGER.info("Unregister Netatmo webhook.") + _LOGGER.info("Unregister Netatmo webhook") 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, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 5163c9582b0..5445231282c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -217,7 +217,7 @@ class NetatmoCamera(NetatmoBase, Camera): return response.content @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the Netatmo-specific camera state attributes.""" return { "id": self._id, @@ -350,7 +350,7 @@ class NetatmoCamera(NetatmoBase, Camera): def _service_set_camera_light(self, **kwargs): """Service to set light mode.""" mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) - _LOGGER.debug("Turn camera '%s' %s", self._name, mode) + _LOGGER.debug("Turn %s camera light for '%s'", mode, self._name) self._data.set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index d12ee9263db..9993b4efac2 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,7 +1,9 @@ """Support for Netatmo Smart thermostats.""" -import logging -from typing import List, Optional +from __future__ import annotations +import logging + +import pyatmo import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -40,6 +42,7 @@ from .const import ( DATA_SCHEDULES, DOMAIN, EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, MANUFACTURER, @@ -121,10 +124,10 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [] for home_id in get_all_home_ids(home_data): - _LOGGER.debug("Setting up home %s ...", home_id) + _LOGGER.debug("Setting up home %s", home_id) for room_id in home_data.rooms[home_id].keys(): room_name = home_data.rooms[home_id][room_id]["name"] - _LOGGER.debug("Setting up room %s (%s) ...", room_name, room_id) + _LOGGER.debug("Setting up room %s (%s)", room_name, room_id) signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" await data_handler.register_data_class( HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id @@ -234,6 +237,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SCHEDULE, ): self._listeners.append( async_dispatcher_connect( @@ -251,7 +255,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Handle webhook events.""" data = event["data"] - if not data.get("home"): + if self._home_id != data["home_id"]: + return + + if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: + self._selected_schedule = self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].get(data["schedule_id"]) + self.async_write_ha_state() + self.data_handler.async_force_update(self._home_status_class) return home = data["home"] @@ -268,35 +280,37 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._target_temperature = self._away_temperature elif self._preset == PRESET_SCHEDULE: self.async_update_callback() + self.data_handler.async_force_update(self._home_status_class) self.async_write_ha_state() return - if not home.get("rooms"): - return - - for room in home["rooms"]: - if data["event_type"] == EVENT_TYPE_SET_POINT: - if self._id == room["id"]: - if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: - self._hvac_mode = HVAC_MODE_OFF - elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX: + for room in home.get("rooms", []): + if data["event_type"] == EVENT_TYPE_SET_POINT and self._id == room["id"]: + if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: + self._hvac_mode = HVAC_MODE_OFF + self._preset = STATE_NETATMO_OFF + self._target_temperature = 0 + elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX: + self._hvac_mode = HVAC_MODE_HEAT + self._preset = PRESET_MAP_NETATMO[PRESET_BOOST] + self._target_temperature = DEFAULT_MAX_TEMP + elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL: + self._hvac_mode = HVAC_MODE_HEAT + self._target_temperature = room["therm_setpoint_temperature"] + else: + self._target_temperature = room["therm_setpoint_temperature"] + if self._target_temperature == DEFAULT_MAX_TEMP: self._hvac_mode = HVAC_MODE_HEAT - self._target_temperature = DEFAULT_MAX_TEMP - elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL: - self._hvac_mode = HVAC_MODE_HEAT - self._target_temperature = room["therm_setpoint_temperature"] - else: - self._target_temperature = room["therm_setpoint_temperature"] - if self._target_temperature == DEFAULT_MAX_TEMP: - self._hvac_mode = HVAC_MODE_HEAT - self.async_write_ha_state() - break + self.async_write_ha_state() + return - elif data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT: - if self._id == room["id"]: - self.async_update_callback() - self.async_write_ha_state() - break + if ( + data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT + and self._id == room["id"] + ): + self.async_update_callback() + self.async_write_ha_state() + return @property def supported_features(self): @@ -319,7 +333,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return self._target_temperature @property - def target_temperature_step(self) -> Optional[float]: + def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" return PRECISION_HALVES @@ -334,7 +348,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return self._operation_list @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" if self._model == NA_THERM and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] @@ -399,12 +413,12 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" return self._preset @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return SUPPORT_PRESET @@ -418,7 +432,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the thermostat.""" attr = {} @@ -569,7 +583,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): schedule_id = sid if not schedule_id: - _LOGGER.error("You passed an invalid schedule") + _LOGGER.error( + "%s is not a invalid schedule", kwargs.get(ATTR_SCHEDULE_NAME) + ) return self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) @@ -586,7 +602,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {**super().device_info, "suggested_area": self._room_data["name"]} -def interpolate(batterylevel, module_type): +def interpolate(batterylevel: int, module_type: str) -> int: """Interpolate battery level depending on device type.""" na_battery_levels = { NA_THERM: { @@ -628,7 +644,7 @@ def interpolate(batterylevel, module_type): return int(pct) -def get_all_home_ids(home_data): +def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: """Get all the home ids returned by NetAtmo API.""" if home_data is None: return [] diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index baee3e4035c..b0a312fa1f3 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -72,7 +72,6 @@ DEFAULT_DISCOVERY = True DEFAULT_WEBHOOKS = False ATTR_PSEUDO = "pseudo" -ATTR_NAME = "name" ATTR_EVENT_TYPE = "event_type" ATTR_HEATING_POWER_REQUEST = "heating_power_request" ATTR_HOME_ID = "home_id" @@ -95,6 +94,7 @@ SERVICE_SET_PERSON_AWAY = "set_person_away" EVENT_TYPE_SET_POINT = "set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" EVENT_TYPE_THERM_MODE = "therm_mode" +EVENT_TYPE_SCHEDULE = "schedule" # Camera events EVENT_TYPE_LIGHT_MODE = "light_mode" EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index be0120bd1a0..6982a651a45 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -1,16 +1,18 @@ """The Netatmo data handler.""" +from __future__ import annotations + from collections import deque from datetime import timedelta from functools import partial from itertools import islice import logging from time import time -from typing import Deque, Dict, List +from typing import Deque import pyatmo from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval @@ -55,8 +57,8 @@ class NetatmoDataHandler: """Initialize self.""" self.hass = hass self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] - self.listeners: List[CALLBACK_TYPE] = [] - self._data_classes: Dict = {} + self.listeners: list[CALLBACK_TYPE] = [] + self._data_classes: dict = {} self.data = {} self._queue: Deque = deque() self._webhook: bool = False @@ -96,6 +98,12 @@ class NetatmoDataHandler: self._queue.rotate(BATCH_SIZE) + @callback + def async_force_update(self, data_class_entry): + """Prioritize data retrieval for given data class entry.""" + self._data_classes[data_class_entry][NEXT_SCAN] = time() + self._queue.rotate(-(self._queue.index(self._data_classes[data_class_entry]))) + async def async_cleanup(self): """Clean up the Netatmo data handler.""" for listener in self.listeners: @@ -113,7 +121,7 @@ class NetatmoDataHandler: elif event["data"]["push_type"] == "NACamera-connection": _LOGGER.debug("%s camera reconnected", MANUFACTURER) - self._data_classes[CAMERA_DATA_CLASS_NAME][NEXT_SCAN] = time() + self.async_force_update(CAMERA_DATA_CLASS_NAME) async def async_fetch_data(self, data_class, data_class_entry, **kwargs): """Fetch data and notify.""" diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 38601e981db..d6085ec06ec 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Netatmo.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -82,7 +82,7 @@ async def async_validate_trigger_config(hass, config): return config -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Netatmo devices.""" registry = await entity_registry.async_get_registry(hass) device_registry = await hass.helpers.device_registry.async_get_registry() @@ -125,8 +125,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index eed12f048c8..08744c462e8 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -31,6 +31,10 @@ async def async_setup_entry(hass, entry, async_add_entities): data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] + await data_handler.register_data_class( + CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None + ) + if CAMERA_DATA_CLASS_NAME not in data_handler.data: raise PlatformNotReady @@ -64,6 +68,8 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(await get_entities(), True) + await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None) + class NetatmoLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 6375c46d394..db00df5129f 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -1,8 +1,9 @@ """Netatmo Media Source Implementation.""" +from __future__ import annotations + import datetime as dt import logging import re -from typing import Optional, Tuple from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, @@ -53,7 +54,7 @@ class NetatmoSource(MediaSource): return PlayMedia(url, MIME_TYPE) async def async_browse_media( - self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES + self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES ) -> BrowseMediaSource: """Return media.""" try: @@ -156,7 +157,7 @@ def remove_html_tags(text): @callback def async_parse_identifier( item: MediaSourceItem, -) -> Tuple[str, str, Optional[int]]: +) -> tuple[str, str, int | None]: """Parse identifier.""" if not item.identifier: return "events", "", None diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 1845cbe76e9..e41b873bdc4 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,6 +1,7 @@ """Base class for Netatmo entities.""" +from __future__ import annotations + import logging -from typing import Dict, List from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import Entity @@ -17,8 +18,8 @@ class NetatmoBase(Entity): def __init__(self, data_handler: NetatmoDataHandler) -> None: """Set up Netatmo entity base.""" self.data_handler = data_handler - self._data_classes: List[Dict] = [] - self._listeners: List[CALLBACK_TYPE] = [] + self._data_classes: list[dict] = [] + self._listeners: list[CALLBACK_TYPE] = [] self._device_name = None self._id = None diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 9176b670bea..4c6facb3eca 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,6 +1,7 @@ """Support for the Netatmo Weather Service.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_LATITUDE, @@ -8,6 +9,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEGREE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, @@ -52,7 +54,7 @@ SUPPORTED_PUBLIC_SENSOR_TYPES = [ SENSOR_TYPES = { "temperature": ["Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, True], "temp_trend": ["Temperature trend", None, "mdi:trending-up", None, False], - "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None, True], + "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, None, DEVICE_CLASS_CO2, True], "pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE, True], "pressure_trend": ["Pressure trend", None, "mdi:trending-up", None, False], "noise": ["Noise", "dB", "mdi:volume-high", None, True], @@ -259,7 +261,7 @@ async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") -class NetatmoSensor(NetatmoBase): +class NetatmoSensor(NetatmoBase, SensorEntity): """Implementation of a Netatmo sensor.""" def __init__(self, data_handler, data_class_name, module_info, sensor_type): @@ -488,7 +490,7 @@ def process_wifi(strength): return "Full" -class NetatmoPublicSensor(NetatmoBase): +class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Represent a single sensor in a Netatmo.""" def __init__(self, data_handler, area, sensor_type): @@ -535,7 +537,7 @@ class NetatmoPublicSensor(NetatmoBase): return self._device_class @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the device.""" attrs = {} @@ -639,7 +641,7 @@ class NetatmoPublicSensor(NetatmoBase): elif self.type == "guststrength": data = self._data.get_latest_gust_strengths() - if not data: + if data is None: if self._state is None: return _LOGGER.debug( @@ -648,8 +650,8 @@ class NetatmoPublicSensor(NetatmoBase): self._state = None return - values = [x for x in data.values() if x is not None] - if self._mode == "avg": - self._state = round(sum(values) / len(values), 1) - elif self._mode == "max": - self._state = max(values) + if values := [x for x in data.values() if x is not None]: + if self._mode == "avg": + self._state = round(sum(values) / len(values), 1) + elif self._mode == "max": + self._state = max(values) diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 0be425d1e31..dccb5857748 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "Abwesenheit", + "hg": "Frostw\u00e4chter", + "schedule": "Zeitplan" + }, + "trigger_type": { + "alarm_started": "{entity_name} hat einen Alarm erkannt", + "animal": "{entity_name} hat ein Tier erkannt", + "cancel_set_point": "{entity_name} hat seinen Zeitplan wieder aufgenommen", + "human": "{entity_name} hat einen Menschen erkannt", + "movement": "{entity_name} hat eine Bewegung erkannt", + "outdoor": "{entity_name} hat ein Ereignis im Freien erkannt", + "person": "{entity_name} hat eine Person erkannt", + "person_away": "{entity_name} hat erkannt, dass eine Person gegangen ist", + "set_point": "Solltemperatur {entity_name} manuell eingestellt", + "therm_mode": "{entity_name} wechselte zu \"{subtype}\"", + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet", + "vehicle": "{entity_name} hat ein Fahrzeug erkannt" + } + }, "options": { "step": { "public_weather": { @@ -30,6 +52,7 @@ }, "public_weather_areas": { "data": { + "new_area": "Bereichsname", "weather_areas": "Wettergebiete" }, "description": "Konfiguriere \u00f6ffentliche Wettersensoren.", diff --git a/homeassistant/components/netatmo/translations/el.json b/homeassistant/components/netatmo/translations/el.json new file mode 100644 index 00000000000..03a1530be9b --- /dev/null +++ b/homeassistant/components/netatmo/translations/el.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "trigger_subtype": { + "away": "\u03b5\u03ba\u03c4\u03cc\u03c2", + "hg": "\u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1 \u03c0\u03b1\u03b3\u03b5\u03c4\u03bf\u03cd", + "schedule": "\u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1" + }, + "trigger_type": { + "alarm_started": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03cc", + "animal": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03b6\u03ce\u03bf", + "cancel_set_point": "\u03a4\u03bf {entity_name} \u03c3\u03c5\u03bd\u03ad\u03c7\u03b9\u03c3\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03ac \u03c4\u03bf\u03c5", + "human": "{entity_name} \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1\u03c2 \u03ac\u03bd\u03b8\u03c1\u03c9\u03c0\u03bf\u03c2", + "movement": "{entity_name} \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "outdoor": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03bf\u03cd \u03c7\u03ce\u03c1\u03bf\u03c5", + "person": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03ac\u03c4\u03bf\u03bc\u03bf", + "person_away": "{entity_name} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03ac\u03c4\u03bf\u03bc\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c6\u03cd\u03b3\u03b5\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index cae1f6d20c0..b4979396eeb 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" @@ -12,5 +14,40 @@ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" } } + }, + "device_automation": { + "trigger_subtype": { + "away": "t\u00e1vol" + }, + "trigger_type": { + "alarm_started": "{entity_name} riaszt\u00e1st \u00e9szlelt", + "animal": "{entity_name} \u00e9szlelt egy \u00e1llatot", + "cancel_set_point": "{entity_name} folytatta \u00fctemez\u00e9s\u00e9t", + "human": "{entity_name} embert \u00e9szlelt", + "movement": "{entity_name} mozg\u00e1st \u00e9szlelt", + "outdoor": "{entity_name} k\u00fclt\u00e9ri esem\u00e9nyt \u00e9szlelt", + "person": "{entity_name} szem\u00e9lyt \u00e9szlelt", + "person_away": "{entity_name} \u00e9szlelte, hogy egy szem\u00e9ly t\u00e1vozott", + "set_point": "A(z) {entity_name} c\u00e9lh\u0151m\u00e9rs\u00e9klet manu\u00e1lisan lett be\u00e1ll\u00edtva", + "therm_mode": "{entity_name} \u00e1tv\u00e1ltott erre: \"{subtype}\"", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva", + "vehicle": "{entity_name} j\u00e1rm\u0171vet \u00e9szlelt" + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "A ter\u00fclet neve" + } + }, + "public_weather_areas": { + "data": { + "new_area": "Ter\u00fclet neve", + "weather_areas": "Id\u0151j\u00e1r\u00e1si ter\u00fcletek" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/id.json b/homeassistant/components/netatmo/translations/id.json new file mode 100644 index 00000000000..6812d45816b --- /dev/null +++ b/homeassistant/components/netatmo/translations/id.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "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" + } + } + }, + "device_automation": { + "trigger_subtype": { + "away": "keluar", + "schedule": "jadwal" + }, + "trigger_type": { + "alarm_started": "{entity_name} mendeteksi alarm", + "animal": "{entity_name} mendeteksi binatang", + "cancel_set_point": "{entity_name} telah melanjutkan jadwalnya", + "human": "{entity_name} mendeteksi manusia", + "movement": "{entity_name} mendeteksi gerakan", + "outdoor": "{entity_name} mendeteksi peristiwa luar ruangan", + "person": "{entity_name} mendeteksi seseorang", + "person_away": "{entity_name} mendeteksi seseorang telah pergi", + "set_point": "Suhu target {entity_name} disetel secara manual", + "therm_mode": "{entity_name} beralih ke \"{subtype}\"", + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan", + "vehicle": "{entity_name} mendeteksi kendaraan" + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Nama area", + "lat_ne": "Lintang Pojok Timur Laut", + "lat_sw": "Lintang Pojok Barat Daya", + "lon_ne": "Bujur Pojok Timur Laut", + "lon_sw": "Bujur Pojok Barat Daya", + "mode": "Perhitungan", + "show_on_map": "Tampilkan di peta" + }, + "description": "Konfigurasikan sensor cuaca publik untuk suatu area.", + "title": "Sensor cuaca publik Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "Nama area", + "weather_areas": "Area cuaca" + }, + "description": "Konfigurasikan sensor cuaca publik.", + "title": "Sensor cuaca publik Netatmo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index 320df466515..fee370ce219 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -4,7 +4,7 @@ "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.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "\uc678\ucd9c", + "hg": "\ub3d9\ud30c \ubc29\uc9c0", + "schedule": "\uc77c\uc815" + }, + "trigger_type": { + "alarm_started": "{entity_name}\uc774(\uac00) \uc54c\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "animal": "{entity_name}\uc774(\uac00) \ub3d9\ubb3c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "cancel_set_point": "{entity_name}\uc774(\uac00) \uc77c\uc815\uc744 \uc7ac\uac1c\ud560 \ub54c", + "human": "{entity_name}\uc774(\uac00) \uc0ac\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "movement": "{entity_name}\uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "outdoor": "{entity_name}\uc774(\uac00) \uc2e4\uc678\uc758 \uc774\ubca4\ud2b8\ub97c \uac10\uc9c0\ud588\uc744 \ub54c", + "person": "{entity_name}\uc774(\uac00) \uc0ac\ub78c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "person_away": "{entity_name}\uc774(\uac00) \uc0ac\ub78c\uc774 \ub5a0\ub0ac\uc74c\uc744 \uac10\uc9c0\ud588\uc744 \ub54c", + "set_point": "{entity_name}\uc758 \ubaa9\ud45c \uc628\ub3c4\uac00 \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ub418\uc5c8\uc744 \ub54c", + "therm_mode": "{entity_name}\uc774(\uac00) {subtype}(\uc73c)\ub85c \uc804\ud658\ub418\uc5c8\uc744 \ub54c", + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c", + "vehicle": "{entity_name}\uc774(\uac00) \ucc28\ub7c9\uc744 \uac10\uc9c0\ud588\uc744 \ub54c" + } + }, "options": { "step": { "public_weather": { @@ -27,16 +49,16 @@ "mode": "\uacc4\uc0b0\ud558\uae30", "show_on_map": "\uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ud558\uae30" }, - "description": "\uc9c0\uc5ed\uc5d0 \ub300\ud55c \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", - "title": "Netamo \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c" + "description": "\uc9c0\uc5ed\uc5d0 \ub300\ud55c \uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Netamo \uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c" }, "public_weather_areas": { "data": { "new_area": "\uc9c0\uc5ed \uc774\ub984", "weather_areas": "\ub0a0\uc528 \uc9c0\uc5ed" }, - "description": "\uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", - "title": "Netamo \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c" + "description": "\uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Netamo \uacf5\uacf5 \uae30\uc0c1 \uc13c\uc11c" } } } diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 0bdc3170a5a..dc811b63534 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -7,7 +7,7 @@ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "create_entry": { - "default": "Succesvol geauthenticeerd met Netatmo." + "default": "Succesvol geauthenticeerd" }, "step": { "pick_implementation": { @@ -48,10 +48,17 @@ "lon_sw": "Lengtegraad Zuidwestelijke hoek", "mode": "Berekening", "show_on_map": "Toon op kaart" - } + }, + "description": "Configureer een openbare weersensor voor een gebied.", + "title": "Netatmo openbare weersensor" }, "public_weather_areas": { - "description": "Configureer openbare weersensoren." + "data": { + "new_area": "Naam van het gebied", + "weather_areas": "Weersgebieden" + }, + "description": "Configureer openbare weersensoren.", + "title": "Netatmo openbare weersensor" } } } diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index b41689a9f8c..449e09bfa3a 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -15,6 +15,28 @@ } } }, + "device_automation": { + "trigger_subtype": { + "away": "poza domem", + "hg": "ochrona przed mrozem", + "schedule": "harmonogram" + }, + "trigger_type": { + "alarm_started": "{entity_name} wykryje alarm", + "animal": "{entity_name} wykryje zwierz\u0119", + "cancel_set_point": "{entity_name} wznowi sw\u00f3j harmonogram", + "human": "{entity_name} wykryje cz\u0142owieka", + "movement": "{entity_name} wykryje ruch", + "outdoor": "{entity_name} wykryje zdarzenie zewn\u0119trzne", + "person": "{entity_name} wykryje osob\u0119", + "person_away": "{entity_name} wykryje, \u017ce osoba wysz\u0142a", + "set_point": "temperatura docelowa {entity_name} zosta\u0142a ustawiona r\u0119cznie", + "therm_mode": "{entity_name} prze\u0142\u0105czy\u0142(a) si\u0119 na \u201e{subtype}\u201d", + "turned_off": "{entity_name} zostanie wy\u0142\u0105czony", + "turned_on": "{entity_name} zostanie w\u0142\u0105czony", + "vehicle": "{entity_name} wykryje pojazd" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/zh-Hans.json b/homeassistant/components/netatmo/translations/zh-Hans.json new file mode 100644 index 00000000000..0e8f01ed8f7 --- /dev/null +++ b/homeassistant/components/netatmo/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "trigger_subtype": { + "away": "\u79bb\u5f00", + "schedule": "\u65e5\u7a0b" + }, + "trigger_type": { + "alarm_started": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u95f9\u949f", + "animal": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u52a8\u7269", + "human": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u4eba", + "outdoor": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u51fa\u95e8\u4e8b\u4ef6", + "person": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u4eba", + "person_away": "{entity_name} \u68c0\u6d4b\u5230\u4e00\u4e2a\u4eba\u79bb\u5f00\u4e86" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index e41f83b8cc0..54db95e9aa0 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -1,14 +1,13 @@ """The Netatmo integration.""" import logging -from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( ATTR_EVENT_TYPE, ATTR_FACE_URL, ATTR_IS_KNOWN, - ATTR_NAME, ATTR_PERSONS, DATA_DEVICE_IDS, DATA_PERSONS, @@ -20,13 +19,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -EVENT_TYPE_MAP = { +SUBEVENT_TYPE_MAP = { "outdoor": "", "therm_mode": "", } -async def handle_webhook(hass, webhook_id, request): +async def async_handle_webhook(hass, webhook_id, request): """Handle webhook callback.""" try: data = await request.json() @@ -38,17 +37,17 @@ async def handle_webhook(hass, webhook_id, request): event_type = data.get(ATTR_EVENT_TYPE) - if event_type in EVENT_TYPE_MAP: - await async_send_event(hass, event_type, data) + if event_type in SUBEVENT_TYPE_MAP: + async_send_event(hass, event_type, data) - for event_data in data.get(EVENT_TYPE_MAP[event_type], []): - await async_evaluate_event(hass, event_data) + for event_data in data.get(SUBEVENT_TYPE_MAP[event_type], []): + async_evaluate_event(hass, event_data) else: - await async_evaluate_event(hass, data) + async_evaluate_event(hass, data) -async def async_evaluate_event(hass, event_data): +def async_evaluate_event(hass, event_data): """Evaluate events from webhook.""" event_type = event_data.get(ATTR_EVENT_TYPE) @@ -62,13 +61,13 @@ async def async_evaluate_event(hass, event_data): person_event_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) person_event_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) - await async_send_event(hass, event_type, person_event_data) + async_send_event(hass, event_type, person_event_data) else: - await async_send_event(hass, event_type, event_data) + async_send_event(hass, event_type, event_data) -async def async_send_event(hass, event_type, data): +def async_send_event(hass, event_type, data): """Send events.""" _LOGGER.debug("%s: %s", event_type, data) async_dispatcher_send( diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 64c8d789fc7..21e4cd1b005 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -6,7 +6,7 @@ from netdata import Netdata from netdata.exceptions import NetdataError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_ICON, @@ -18,7 +18,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -97,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class NetdataSensor(Entity): +class NetdataSensor(SensorEntity): """Implementation of a Netdata sensor.""" def __init__(self, netdata, name, sensor, sensor_name, element, icon, unit, invert): @@ -146,7 +145,7 @@ class NetdataSensor(Entity): ) -class NetdataAlarms(Entity): +class NetdataAlarms(SensorEntity): """Implementation of a Netdata alarm sensor.""" def __init__(self, netdata, name, host, port): diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index abbedf79d64..c8f07301e98 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -1,5 +1,5 @@ """Support for Netgear LTE sensors.""" -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.exceptions import PlatformNotReady from . import CONF_MONITORED_CONDITIONS, DATA_KEY, LTEEntity @@ -33,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info) async_add_entities(sensors) -class LTESensor(LTEEntity): +class LTESensor(LTEEntity, SensorEntity): """Base LTE sensor entity.""" @property diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 3c1482844ad..a254d06fc06 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -169,7 +169,7 @@ class NetioSwitch(SwitchEntity): self.netio.update() @property - def state_attributes(self): + def extra_state_attributes(self): """Return optional state attributes.""" return { ATTR_TOTAL_CONSUMPTION_KWH: self.cumulated_consumption_kwh, diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 264d7508347..2bc17fbecb2 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -6,10 +6,9 @@ import neurio import requests.exceptions import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -123,7 +122,7 @@ class NeurioData: self._daily_usage = round(kwh, 2) -class NeurioEnergy(Entity): +class NeurioEnergy(SensorEntity): """Implementation of a Neurio energy sensor.""" def __init__(self, data, name, sensor_type, update_call): diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index fc2ef7fef35..4dde2084400 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -80,9 +80,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UPDATE_COORDINATOR: coordinator, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -93,8 +93,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 2b3f7de4489..aff3711cdae 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -334,12 +334,19 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): new_cool_temp = min_temp + deadband # Check that we're within the deadband range, fix it if we're not - if new_heat_temp and new_heat_temp != cur_heat_temp: - if new_cool_temp - new_heat_temp < deadband: - new_cool_temp = new_heat_temp + deadband - if new_cool_temp and new_cool_temp != cur_cool_temp: - if new_cool_temp - new_heat_temp < deadband: - new_heat_temp = new_cool_temp - deadband + if ( + new_heat_temp + and new_heat_temp != cur_heat_temp + and new_cool_temp - new_heat_temp < deadband + ): + new_cool_temp = new_heat_temp + deadband + + if ( + new_cool_temp + and new_cool_temp != cur_cool_temp + and new_cool_temp - new_heat_temp < deadband + ): + new_heat_temp = new_cool_temp - deadband self._zone.set_heat_cool_temp( heat_temperature=new_heat_temp, @@ -354,9 +361,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return self._thermostat.is_emergency_heat_active() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" - data = super().device_state_attributes + data = super().extra_state_attributes data[ATTR_ZONE_STATUS] = self._zone.get_status() diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index 87850145077..e68564706fa 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 62f6e8275c4..fc69c7ef389 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -33,7 +33,7 @@ class NexiaEntity(CoordinatorEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index d3a6691d59e..495a8fb4d3a 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -41,9 +41,9 @@ class NexiaAutomationScene(NexiaEntity, Scene): self._automation = automation @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the scene specific state attributes.""" - data = super().device_state_attributes + data = super().extra_state_attributes data[ATTR_DESCRIPTION] = self._automation.description return data diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index eff15d443bc..a14931e41ee 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -2,6 +2,7 @@ from nexia.const import UNIT_CELSIUS +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -149,7 +150,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NexiaThermostatSensor(NexiaThermostatEntity): +class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): """Provides Nexia thermostat sensor support.""" def __init__( @@ -196,7 +197,7 @@ class NexiaThermostatSensor(NexiaThermostatEntity): return self._unit_of_measurement -class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity): +class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): """Nexia Zone Sensor Support.""" def __init__( diff --git a/homeassistant/components/nexia/translations/he.json b/homeassistant/components/nexia/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/nexia/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/hu.json b/homeassistant/components/nexia/translations/hu.json index dee4ed9ee0f..7dedf459484 100644 --- a/homeassistant/components/nexia/translations/hu.json +++ b/homeassistant/components/nexia/translations/hu.json @@ -1,11 +1,20 @@ { "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" + }, "step": { "user": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Csatlakoz\u00e1s a mynexia.com-hoz" } } } diff --git a/homeassistant/components/nexia/translations/id.json b/homeassistant/components/nexia/translations/id.json new file mode 100644 index 00000000000..e6900bdffa1 --- /dev/null +++ b/homeassistant/components/nexia/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": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/ko.json b/homeassistant/components/nexia/translations/ko.json index 170411f5f73..4bd1589e4e8 100644 --- a/homeassistant/components/nexia/translations/ko.json +++ b/homeassistant/components/nexia/translations/ko.json @@ -14,7 +14,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "title": "mynexia.com \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "mynexia.com\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json index d718c78d7af..faa19d3b63c 100644 --- a/homeassistant/components/nexia/translations/nl.json +++ b/homeassistant/components/nexia/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Deze nexia-woning is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json index f1c7b5b8ced..74ec08ec2cf 100644 --- a/homeassistant/components/nexia/translations/ru.json +++ b/homeassistant/components/nexia/translations/ru.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 mynexia.com" } diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 2b5da2a97fa..67d0a4a81d7 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -5,10 +5,9 @@ import logging from py_nextbus import NextBusClient import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, DEVICE_CLASS_TIMESTAMP import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp _LOGGER = logging.getLogger(__name__) @@ -104,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([NextBusDepartureSensor(client, agency, route, stop, name)], True) -class NextBusDepartureSensor(Entity): +class NextBusDepartureSensor(SensorEntity): """Sensor class that displays upcoming NextBus times. To function, this requires knowing the agency tag as well as the tags for @@ -156,7 +155,7 @@ class NextBusDepartureSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return additional state attributes.""" return self._attributes diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 1a773040980..efa5b2e2f32 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) DOMAIN = "nextcloud" -NEXTCLOUD_COMPONENTS = ("sensor", "binary_sensor") +PLATFORMS = ("sensor", "binary_sensor") SCAN_INTERVAL = timedelta(seconds=60) # Validate user configuration @@ -116,8 +116,8 @@ def setup(hass, config): # Update sensors on time interval track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL]) - for component in NEXTCLOUD_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index e0be9dde69e..5cd02f124e9 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -1,5 +1,5 @@ """Summary data from Nextcoud.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from . import DOMAIN, SENSORS @@ -15,7 +15,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class NextcloudSensor(Entity): +class NextcloudSensor(SensorEntity): """Represents a Nextcloud sensor.""" def __init__(self, item): diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 4c8ab756ddf..dfaaf28048e 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -48,9 +48,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_type="service", ) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -61,8 +61,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 3000d652e46..2b91395d377 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_API_KEY, CONF_URL -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN from .utils import hash_from_url _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index efa625577d9..ea2ea549cec 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -1,12 +1,15 @@ """Support for Nightscout sensors.""" +from __future__ import annotations + from asyncio import TimeoutError as AsyncIOTimeoutError from datetime import timedelta import logging -from typing import Callable, List +from typing import Callable from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant @@ -24,14 +27,14 @@ DEFAULT_NAME = "Blood Glucose" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the Glucose Sensor.""" api = hass.data[DOMAIN][entry.entry_id] async_add_entities([NightscoutSensor(api, "Blood Sugar", entry.unique_id)], True) -class NightscoutSensor(Entity): +class NightscoutSensor(SensorEntity): """Implementation of a Nightscout sensor.""" def __init__(self, api: NightscoutAPI, name, unique_id): @@ -115,6 +118,6 @@ class NightscoutSensor(Entity): return switcher.get(self._attributes[ATTR_DIRECTION], "mdi:cloud-question") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json index 3b2d79a34a7..459a879e82c 100644 --- a/homeassistant/components/nightscout/translations/hu.json +++ b/homeassistant/components/nightscout/translations/hu.json @@ -2,6 +2,20 @@ "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": "Nightscout", + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "url": "URL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/id.json b/homeassistant/components/nightscout/translations/id.json new file mode 100644 index 00000000000..75496084bc4 --- /dev/null +++ b/homeassistant/components/nightscout/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "url": "URL" + }, + "description": "- URL: alamat instans nightscout Anda, misalnya https://myhomeassistant.duckdns.org:5423\n- Kunci API (Opsional): Hanya gunakan jika instans Anda dilindungi (auth_default_roles != dapat dibaca).", + "title": "Masukkan informasi server Nightscout Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json index 0408a1f61ab..1146e6926e3 100644 --- a/homeassistant/components/nightscout/translations/ko.json +++ b/homeassistant/components/nightscout/translations/ko.json @@ -8,12 +8,15 @@ "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": "Nightscout", "step": { "user": { "data": { "api_key": "API \ud0a4", "url": "URL \uc8fc\uc18c" - } + }, + "description": "- URL: Nightscout \uc778\uc2a4\ud134\uc2a4\uc758 \uc8fc\uc18c. \uc608: https://myhomeassistant.duckdns.org:5423\n- API \ud0a4 (\uc120\ud0dd \uc0ac\ud56d): \uc778\uc2a4\ud134\uc2a4\uac00 \ubcf4\ud638\ub41c \uacbd\uc6b0\uc5d0\ub9cc \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694 (auth_default_roles != readable).", + "title": "Nightscout \uc11c\ubc84 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/nightscout/translations/nl.json b/homeassistant/components/nightscout/translations/nl.json index 0146996dce5..a9b81e9403e 100644 --- a/homeassistant/components/nightscout/translations/nl.json +++ b/homeassistant/components/nightscout/translations/nl.json @@ -8,12 +8,15 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, + "flow_title": "Nightscout", "step": { "user": { "data": { "api_key": "API-sleutel", "url": "URL" - } + }, + "description": "- URL: het adres van uw nightscout instantie. Bijv.: https://myhomeassistant.duckdns.org:5423\n- API-sleutel (optioneel): Alleen gebruiken als uw instantie beveiligd is (auth_default_roles != readable).", + "title": "Voer uw Nightscout-serverinformatie in." } } } diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 8e851592de3..d6fcad3ac7e 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -175,7 +175,7 @@ class NiluSensor(AirQualityEntity): return ATTRIBUTION @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return other details about the sensor state.""" return self._attrs diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 6417926702d..24adf223719 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -81,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor"] +PLATFORMS = ["sensor", "switch", "binary_sensor"] SIGNAL_UPDATE_LEAF = "nissan_leaf_update" @@ -94,7 +94,7 @@ START_CHARGE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) def setup(hass, config): - """Set up the Nissan Leaf component.""" + """Set up the Nissan Leaf integration.""" async def async_handle_update(service): """Handle service to update leaf data from Nissan servers.""" @@ -135,7 +135,7 @@ def setup(hass, config): def setup_leaf(car_config): """Set up a car.""" - _LOGGER.debug("Logging into You+Nissan...") + _LOGGER.debug("Logging into You+Nissan") username = car_config[CONF_USERNAME] password = car_config[CONF_PASSWORD] @@ -170,8 +170,8 @@ def setup(hass, config): data_store = LeafDataStore(hass, leaf, car_config) hass.data[DATA_LEAF][leaf.vin] = data_store - for component in LEAF_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, car_config) + for platform in PLATFORMS: + load_platform(hass, platform, DOMAIN, {}, car_config) async_track_point_in_utc_time( hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE @@ -450,7 +450,7 @@ class LeafEntity(Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return default attributes for Nissan leaf entities.""" return { "next_update": self.car.next_update, diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 368db17ab4b..936d607a84e 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -1,6 +1,7 @@ """Battery Charge and Range Support for the Nissan Leaf.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES @@ -35,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices, True) -class LeafBatterySensor(LeafEntity): +class LeafBatterySensor(LeafEntity, SensorEntity): """Nissan Leaf Battery Sensor.""" @property @@ -65,7 +66,7 @@ class LeafBatterySensor(LeafEntity): return icon_for_battery_level(battery_level=self.state, charging=chargestate) -class LeafRangeSensor(LeafEntity): +class LeafRangeSensor(LeafEntity, SensorEntity): """Nissan Leaf Range Sensor.""" def __init__(self, car, ac_on): diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index d95d3e4ed39..2b8d557c2dd 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -37,9 +37,9 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return climate control attributes.""" - attrs = super().device_state_attributes + attrs = super().extra_state_attributes attrs["updated_on"] = self.car.last_climate_response return attrs diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 608f90d5421..69c65873e51 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -89,7 +89,7 @@ class NmapDeviceScanner(DeviceScanner): Returns boolean if scanning successful. """ - _LOGGER.debug("Scanning...") + _LOGGER.debug("Scanning") scanner = PortScanner() diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index bdf4658434c..32e4fd87e29 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -4,7 +4,7 @@ import logging from pyrail import iRail import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -14,7 +14,6 @@ from homeassistant.const import ( TIME_MINUTES, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -88,7 +87,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class NMBSLiveBoard(Entity): +class NMBSLiveBoard(SensorEntity): """Get the next train from a station's liveboard.""" def __init__(self, api_client, live_station, station_from, station_to): @@ -126,7 +125,7 @@ class NMBSLiveBoard(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the sensor attributes if data is available.""" if self._state is None or not self._attrs: return None @@ -164,7 +163,7 @@ class NMBSLiveBoard(Entity): ) -class NMBSSensor(Entity): +class NMBSSensor(SensorEntity): """Get the the total travel time for a given connection.""" def __init__( @@ -202,7 +201,7 @@ class NMBSSensor(Entity): return "mdi:train" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return sensor attributes if data is available.""" if self._state is None or not self._attrs: return None diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index a0453e3acb1..e637e953173 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -2,11 +2,11 @@ from datetime import datetime, timedelta import logging -import noaa_coops as coops # pylint: disable=import-error +import noaa_coops as coops import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_NAME, @@ -15,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -72,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([noaa_sensor], True) -class NOAATidesAndCurrentsSensor(Entity): +class NOAATidesAndCurrentsSensor(SensorEntity): """Representation of a NOAA Tides and Currents sensor.""" def __init__(self, name, station_id, timezone, unit_system, station): @@ -90,7 +89,7 @@ class NOAATidesAndCurrentsSensor(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of this device.""" attr = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} if self.data is None: diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 8e6c13260e5..788f900ef70 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -80,7 +80,7 @@ class AirSensor(AirQualityEntity): return ATTRIBUTION @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return other details about the sensor state.""" return { "level": self._api.data.get("level"), diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 7be66dc3c59..e64ceb48a21 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,19 +1,20 @@ """Provides functionality to notify people.""" +from __future__ import annotations + import asyncio from functools import partial import logging -from typing import Any, Dict, Optional, cast +from typing import Any, cast import voluptuous as vol import homeassistant.components.persistent_notification as pn -from homeassistant.const import CONF_NAME, CONF_PLATFORM -from homeassistant.core import ServiceCall +from homeassistant.const import CONF_DESCRIPTION, CONF_NAME, CONF_PLATFORM +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError 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.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify @@ -43,7 +44,6 @@ SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification" NOTIFY_SERVICES = "notify_services" -CONF_DESCRIPTION = "description" CONF_FIELDS = "fields" PLATFORM_SCHEMA = vol.Schema( @@ -69,7 +69,7 @@ PERSISTENT_NOTIFICATION_SERVICE_SCHEMA = vol.Schema( @bind_hass -async def async_reload(hass: HomeAssistantType, integration_name: str) -> None: +async def async_reload(hass: HomeAssistant, integration_name: str) -> None: """Register notify services for an integration.""" if not _async_integration_has_notify_services(hass, integration_name): return @@ -83,7 +83,7 @@ async def async_reload(hass: HomeAssistantType, integration_name: str) -> None: @bind_hass -async def async_reset_platform(hass: HomeAssistantType, integration_name: str) -> None: +async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Unregister notify services for an integration.""" if not _async_integration_has_notify_services(hass, integration_name): return @@ -99,7 +99,7 @@ async def async_reset_platform(hass: HomeAssistantType, integration_name: str) - def _async_integration_has_notify_services( - hass: HomeAssistantType, integration_name: str + hass: HomeAssistant, integration_name: str ) -> bool: """Determine if an integration has notify services registered.""" if ( @@ -114,9 +114,13 @@ def _async_integration_has_notify_services( class BaseNotificationService: """An abstract class for notification services.""" - hass: Optional[HomeAssistantType] = None + # While not purely typed, it makes typehinting more useful for us + # and removes the need for constant None checks or asserts. + # Ignore types: https://github.com/PyCQA/pylint/issues/3167 + hass: HomeAssistant = None # type: ignore + # Name => target - registered_targets: Dict[str, str] + registered_targets: dict[str, str] def send_message(self, message, **kwargs): """Send a message. @@ -130,7 +134,9 @@ class BaseNotificationService: kwargs can contain ATTR_TITLE to specify a title. """ - await self.hass.async_add_executor_job(partial(self.send_message, message, **kwargs)) # type: ignore + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) async def _async_notify_message_service(self, service: ServiceCall) -> None: """Handle sending notification message service calls.""" @@ -155,7 +161,7 @@ class BaseNotificationService: async def async_setup( self, - hass: HomeAssistantType, + hass: HomeAssistant, service_name: str, target_service_name_prefix: str, ) -> None: @@ -175,8 +181,6 @@ class BaseNotificationService: async def async_register_services(self) -> None: """Create or update the notify services.""" - assert self.hass - if hasattr(self, "targets"): stale_targets = set(self.registered_targets) @@ -232,8 +236,6 @@ class BaseNotificationService: async def async_unregister_services(self) -> None: """Unregister the notify services.""" - assert self.hass - if self.registered_targets: remove_targets = set(self.registered_targets) for remove_target_name in remove_targets: diff --git a/homeassistant/components/notify/translations/hu.json b/homeassistant/components/notify/translations/hu.json index b5c88047f66..18413724b53 100644 --- a/homeassistant/components/notify/translations/hu.json +++ b/homeassistant/components/notify/translations/hu.json @@ -1,3 +1,3 @@ { - "title": "\u00c9rtes\u00edt" + "title": "\u00c9rtes\u00edt\u00e9sek" } \ No newline at end of file diff --git a/homeassistant/components/notify/translations/id.json b/homeassistant/components/notify/translations/id.json index 723b49fe6af..0ee3a77f9c1 100644 --- a/homeassistant/components/notify/translations/id.json +++ b/homeassistant/components/notify/translations/id.json @@ -1,3 +1,3 @@ { - "title": "Pemberitahuan" + "title": "Notifikasi" } \ No newline at end of file diff --git a/homeassistant/components/notify/translations/nl.json b/homeassistant/components/notify/translations/nl.json index 409692f7227..52a24cc9efd 100644 --- a/homeassistant/components/notify/translations/nl.json +++ b/homeassistant/components/notify/translations/nl.json @@ -1,3 +1,3 @@ { - "title": "Notificeer" + "title": "Meldingen" } \ No newline at end of file diff --git a/homeassistant/components/notify_events/notify.py b/homeassistant/components/notify_events/notify.py index 23df01a128b..ce7c353badb 100644 --- a/homeassistant/components/notify_events/notify.py +++ b/homeassistant/components/notify_events/notify.py @@ -28,6 +28,8 @@ ATTR_FILE_MIME_TYPE = "mime_type" ATTR_FILE_KIND_FILE = "file" ATTR_FILE_KIND_IMAGE = "image" +ATTR_TOKEN = "token" + _LOGGER = logging.getLogger(__name__) @@ -114,7 +116,12 @@ class NotifyEventsNotificationService(BaseNotificationService): def send_message(self, message, **kwargs): """Send a message.""" + token = self.token data = kwargs.get(ATTR_DATA) or {} msg = self.prepare_message(message, data) - msg.send(self.token) + + if data.get(ATTR_TOKEN, "").trim(): + token = data[ATTR_TOKEN] + + msg.send(token) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index c2cbdb85289..ca0ccf08c89 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -97,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update, ) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() for platform in PLATFORMS: hass.async_create_task( @@ -180,7 +180,7 @@ class NotionEntity(CoordinatorEntity): return self._device_class @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 86c51e3c13c..425357c3105 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 978e0aac46a..4f034408fe2 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,6 +1,7 @@ """Support for Notion sensors.""" from typing import Callable +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback @@ -42,7 +43,7 @@ async def async_setup_entry( async_add_entities(sensor_list) -class NotionSensor(NotionEntity): +class NotionSensor(NotionEntity, SensorEntity): """Define a Notion sensor.""" def __init__( diff --git a/homeassistant/components/notion/translations/he.json b/homeassistant/components/notion/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/notion/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json index 6bf20925535..b4d57f83bb3 100644 --- a/homeassistant/components/notion/translations/hu.json +++ b/homeassistant/components/notion/translations/hu.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban" }, "step": { diff --git a/homeassistant/components/notion/translations/id.json b/homeassistant/components/notion/translations/id.json new file mode 100644 index 00000000000..35ee7a29544 --- /dev/null +++ b/homeassistant/components/notion/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "no_devices": "Tidak ada perangkat yang ditemukan di akun" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Isi informasi Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index 4b6597725ef..acb42046c90 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze gebruikersnaam is al in gebruik." + "already_configured": "Account is al geconfigureerd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -11,7 +11,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "Gebruikersnaam/E-mailadres" + "username": "Gebruikersnaam" }, "title": "Vul uw gegevens informatie" } diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json index 737539424b0..4b9a45bbf3f 100644 --- a/homeassistant/components/notion/translations/ru.json +++ b/homeassistant/components/notion/translations/ru.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Notion" } diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index b6c0d1a5d9b..6c8061294e9 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -1,15 +1,15 @@ """Sensor platform to display the current fuel prices at a NSW fuel station.""" +from __future__ import annotations + import datetime import logging -from typing import Optional from nsw_fuel import FuelCheckClient, FuelCheckError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -145,7 +145,7 @@ class StationPriceData: return self._station_name -class StationPriceSensor(Entity): +class StationPriceSensor(SensorEntity): """Implementation of a sensor that reports the fuel price for a station.""" def __init__(self, station_data: StationPriceData, fuel_type: str): @@ -159,7 +159,7 @@ class StationPriceSensor(Entity): return f"{self._station_data.get_station_name()} {self._fuel_type}" @property - def state(self) -> Optional[float]: + def state(self) -> float | None: """Return the state of the sensor.""" price_info = self._station_data.for_fuel_type(self._fuel_type) if price_info: @@ -168,7 +168,7 @@ class StationPriceSensor(Entity): return None @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes of the device.""" return { ATTR_STATION_ID: self._station_data.station_id, 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 12ae9d8990a..08e62e6c6a3 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -1,7 +1,8 @@ """Support for NSW Rural Fire Service Feeds.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager import voluptuous as vol @@ -259,22 +260,22 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -284,7 +285,7 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): return LENGTH_KILOMETERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 2f51ae377a5..9fe4764e1af 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -80,9 +80,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -93,8 +93,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index 40de3844620..2cbe1105e97 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -13,8 +13,7 @@ from homeassistant.const import ( HTTP_INTERNAL_SERVER_ERROR, ) -from .const import CONF_SERIAL_NUMBER -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_SERIAL_NUMBER, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nuheat/translations/he.json b/homeassistant/components/nuheat/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/nuheat/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/hu.json b/homeassistant/components/nuheat/translations/hu.json index 8c523b72e04..e6e7174e325 100644 --- a/homeassistant/components/nuheat/translations/hu.json +++ b/homeassistant/components/nuheat/translations/hu.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "cannot_connect": "A csatlakoz\u00e1s nem siker\u00fclt, pr\u00f3b\u00e1lkozzon \u00fajra", - "invalid_thermostat": "A termoszt\u00e1t sorozatsz\u00e1ma \u00e9rv\u00e9nytelen." + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_thermostat": "A termoszt\u00e1t sorozatsz\u00e1ma \u00e9rv\u00e9nytelen.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/nuheat/translations/id.json b/homeassistant/components/nuheat/translations/id.json new file mode 100644 index 00000000000..69041c7755d --- /dev/null +++ b/homeassistant/components/nuheat/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_thermostat": "Nomor seri termostat tidak valid.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "serial_number": "Nomor seri termostat.", + "username": "Nama Pengguna" + }, + "description": "Anda harus mendapatkan nomor seri atau ID numerik termostat Anda dengan masuk ke https://MyNuHeat.com dan memilih termostat Anda.", + "title": "Hubungkan ke NuHeat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/ko.json b/homeassistant/components/nuheat/translations/ko.json index a533cd69093..b926d1f5269 100644 --- a/homeassistant/components/nuheat/translations/ko.json +++ b/homeassistant/components/nuheat/translations/ko.json @@ -16,8 +16,8 @@ "serial_number": "\uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "https://MyNuHeat.com \uc5d0 \ub85c\uadf8\uc778\ud558\uace0 \uc628\ub3c4 \uc870\uc808\uae30\ub97c \uc120\ud0dd\ud558\uc5ec \uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638 \ub610\ub294 \ub610\ub294 ID \ub97c \uc5bb\uc5b4\uc57c \ud569\ub2c8\ub2e4.", - "title": "NuHeat \uc5d0 \uc5f0\uacb0\ud558\uae30" + "description": "https://MyNuHeat.com \uc5d0 \ub85c\uadf8\uc778\ud558\uace0 \uc628\ub3c4 \uc870\uc808\uae30\ub97c \uc120\ud0dd\ud558\uc5ec \uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638 \ub610\ub294 \ub610\ub294 ID\ub97c \uc5bb\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "title": "NuHeat\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/nuheat/translations/nl.json b/homeassistant/components/nuheat/translations/nl.json index edf3ad17ff4..d7672832db0 100644 --- a/homeassistant/components/nuheat/translations/nl.json +++ b/homeassistant/components/nuheat/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "De thermostaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "invalid_thermostat": "Het serienummer van de thermostaat is ongeldig.", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/nuheat/translations/ru.json b/homeassistant/components/nuheat/translations/ru.json index 099f6c3f1fc..7c90b861564 100644 --- a/homeassistant/components/nuheat/translations/ru.json +++ b/homeassistant/components/nuheat/translations/ru.json @@ -14,7 +14,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "serial_number": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0438\u043b\u0438 ID \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430, \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://MyNuHeat.com.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 4af3e0d8ed4..6aa945a52bf 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -11,7 +11,7 @@ import homeassistant.helpers.config_validation as cv from .const import DEFAULT_PORT, DOMAIN -NUKI_PLATFORMS = ["lock"] +PLATFORMS = ["lock"] UPDATE_INTERVAL = timedelta(seconds=30) NUKI_SCHEMA = vol.Schema( @@ -29,7 +29,7 @@ async def async_setup(hass, config): """Set up the Nuki component.""" hass.data.setdefault(DOMAIN, {}) - for platform in NUKI_PLATFORMS: + for platform in PLATFORMS: confs = config.get(platform) if confs is None: continue diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index f065f1c27ef..7d7a846aa80 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -10,11 +10,7 @@ from homeassistant import config_entries, exceptions from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN -from .const import ( # pylint: disable=unused-import - DEFAULT_PORT, - DEFAULT_TIMEOUT, - DOMAIN, -) +from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 818784a2b2e..360153d14fe 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -99,7 +99,7 @@ class NukiDeviceEntity(LockEntity, ABC): """Return true if lock is locked.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" data = { ATTR_BATTERY_CRITICAL: self._nuki_device.battery_critical, diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/nuki/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/hu.json b/homeassistant/components/nuki/translations/hu.json new file mode 100644 index 00000000000..4f0b1a29738 --- /dev/null +++ b/homeassistant/components/nuki/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port", + "token": "Hozz\u00e1f\u00e9r\u00e9si token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/id.json b/homeassistant/components/nuki/translations/id.json new file mode 100644 index 00000000000..d9e5e1de2c3 --- /dev/null +++ b/homeassistant/components/nuki/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "token": "Token Akses" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index 4b7dcd9e372..6138f401ec2 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -2,6 +2,6 @@ "domain": "numato", "name": "Numato USB GPIO Expander", "documentation": "https://www.home-assistant.io/integrations/numato", - "requirements": ["numato-gpio==0.8.0"], + "requirements": ["numato-gpio==0.10.0"], "codeowners": ["@clssn"] } diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index e268d32a293..19372de5258 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -3,8 +3,8 @@ import logging from numato_gpio import NumatoGpioError +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_ID, CONF_NAME, CONF_SENSORS -from homeassistant.helpers.entity import Entity from . import ( CONF_DEVICES, @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class NumatoGpioAdc(Entity): +class NumatoGpioAdc(SensorEntity): """Represents an ADC port of a Numato USB GPIO expander.""" def __init__(self, name, device_id, port, src_range, dst_range, dst_unit, api): diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 31a0bcd7762..e61398f6582 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,8 +1,10 @@ """Component to allow numeric input for platforms.""" +from __future__ import annotations + from abc import abstractmethod from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any import voluptuous as vol @@ -66,7 +68,7 @@ class NumberEntity(Entity): """Representation of a Number entity.""" @property - def capability_attributes(self) -> Dict[str, Any]: + def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" return { ATTR_MIN: self.min_value, @@ -110,5 +112,4 @@ class NumberEntity(Entity): async def async_set_value(self, value: float) -> None: """Set new value.""" - assert self.hass is not None await self.hass.async_add_executor_job(self.set_value, value) diff --git a/homeassistant/components/number/device_action.py b/homeassistant/components/number/device_action.py index c22ba720e37..77b36b49f20 100644 --- a/homeassistant/components/number/device_action.py +++ b/homeassistant/components/number/device_action.py @@ -1,5 +1,7 @@ """Provides device actions for Number.""" -from typing import Any, Dict, List, Optional +from __future__ import annotations + +from typing import Any import voluptuous as vol @@ -27,10 +29,10 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Number.""" registry = await entity_registry.async_get_registry(hass) - actions: List[Dict[str, Any]] = [] + actions: list[dict[str, Any]] = [] # Get all the integrations entities for this device for entry in entity_registry.async_entries_for_device(registry, device_id): @@ -50,14 +52,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - - if config[CONF_TYPE] != ATYP_SET_VALUE: - return - await hass.services.async_call( DOMAIN, const.SERVICE_SET_VALUE, @@ -72,11 +69,6 @@ async def async_call_action_from_config( async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> dict: """List action capabilities.""" - action_type = config[CONF_TYPE] - - if action_type != ATYP_SET_VALUE: - return {} - fields = {vol.Required(const.ATTR_VALUE): vol.Coerce(float)} return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index 611744e3191..4364dffe1e8 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce a Number entity state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State @@ -16,8 +18,8 @@ async def _async_reproduce_state( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -50,8 +52,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce multiple Number states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/number/translations/de.json b/homeassistant/components/number/translations/de.json new file mode 100644 index 00000000000..3ef9a0358a4 --- /dev/null +++ b/homeassistant/components/number/translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Wert f\u00fcr {entity_name} setzen" + } + }, + "title": "Nummer" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/hu.json b/homeassistant/components/number/translations/hu.json new file mode 100644 index 00000000000..296b9b750a7 --- /dev/null +++ b/homeassistant/components/number/translations/hu.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} \u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Sz\u00e1m" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/id.json b/homeassistant/components/number/translations/id.json new file mode 100644 index 00000000000..6e928cb51cf --- /dev/null +++ b/homeassistant/components/number/translations/id.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Tetapkan nilai untuk {entity_name}" + } + }, + "title": "Bilangan" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/ko.json b/homeassistant/components/number/translations/ko.json new file mode 100644 index 00000000000..9c642931408 --- /dev/null +++ b/homeassistant/components/number/translations/ko.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name}\uc758 \uac12 \uc124\uc815\ud558\uae30" + } + }, + "title": "\uc22b\uc790" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/zh-Hans.json b/homeassistant/components/number/translations/zh-Hans.json new file mode 100644 index 00000000000..de9720ed77a --- /dev/null +++ b/homeassistant/components/number/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "set_value": "\u8bbe\u7f6e {entity_name} \u7684\u503c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 5669b8a5c3b..be86ca5951c 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() status = data.status if not status: @@ -101,9 +101,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -178,8 +178,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 7407958cdc0..07e135b8ebd 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import ( CONF_ALIAS, + CONF_BASE, CONF_HOST, CONF_PASSWORD, CONF_PORT, @@ -21,12 +22,12 @@ from .const import ( DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, + DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, SENSOR_NAME, SENSOR_TYPES, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -211,10 +212,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(self.hass, config) except CannotConnect: - errors["base"] = "cannot_connect" + errors[CONF_BASE] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + errors[CONF_BASE] = "unknown" return info, errors @staticmethod @@ -241,7 +242,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) - info = await validate_input(self.hass, self.config_entry.data) + errors = {} + try: + info = await validate_input(self.hass, self.config_entry.data) + except CannotConnect: + errors[CONF_BASE] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors[CONF_BASE] = "unknown" + + if errors: + return self.async_show_form(step_id="abort", errors=errors) base_schema = _resource_schema_base(info["available_resources"], resources) base_schema[ @@ -249,10 +260,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ] = cv.positive_int return self.async_show_form( - step_id="init", - data_schema=vol.Schema(base_schema), + step_id="init", data_schema=vol.Schema(base_schema), errors=errors ) + async def async_step_abort(self, user_input=None): + """Abort options flow.""" + return self.async_create_entry(title="", data=self.config_entry.options) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index cc70b33f763..890ac3697dd 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -1,8 +1,10 @@ """The nut component.""" from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, ) from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, @@ -45,7 +47,7 @@ SENSOR_TYPES = { "ups.temperature": [ "UPS Temperature", TEMP_CELSIUS, - "mdi:thermometer", + None, DEVICE_CLASS_TEMPERATURE, ], "ups.load": ["Load", PERCENTAGE, "mdi:gauge", None], @@ -83,13 +85,13 @@ SENSOR_TYPES = { "ups.realpower": [ "Current Real Power", POWER_WATT, - "mdi:flash", + None, DEVICE_CLASS_POWER, ], "ups.realpower.nominal": [ "Nominal Real Power", POWER_WATT, - "mdi:flash", + None, DEVICE_CLASS_POWER, ], "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline", None], @@ -102,7 +104,7 @@ SENSOR_TYPES = { "battery.charge": [ "Battery Charge", PERCENTAGE, - "mdi:gauge", + None, DEVICE_CLASS_BATTERY, ], "battery.charge.low": ["Low Battery Setpoint", PERCENTAGE, "mdi:gauge", None], @@ -119,10 +121,15 @@ SENSOR_TYPES = { None, ], "battery.charger.status": ["Charging Status", "", "mdi:information-outline", None], - "battery.voltage": ["Battery Voltage", VOLT, "mdi:flash", None], - "battery.voltage.nominal": ["Nominal Battery Voltage", VOLT, "mdi:flash", None], - "battery.voltage.low": ["Low Battery Voltage", VOLT, "mdi:flash", None], - "battery.voltage.high": ["High Battery Voltage", VOLT, "mdi:flash", None], + "battery.voltage": ["Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], + "battery.voltage.nominal": [ + "Nominal Battery Voltage", + VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], + "battery.voltage.low": ["Low Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], + "battery.voltage.high": ["High Battery Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash", None], "battery.current": [ "Battery Current", @@ -139,7 +146,7 @@ SENSOR_TYPES = { "battery.temperature": [ "Battery Temperature", TEMP_CELSIUS, - "mdi:thermometer", + None, DEVICE_CLASS_TEMPERATURE, ], "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer-outline", None], @@ -177,16 +184,21 @@ SENSOR_TYPES = { "mdi:information-outline", None, ], - "input.transfer.low": ["Low Voltage Transfer", VOLT, "mdi:flash", None], - "input.transfer.high": ["High Voltage Transfer", VOLT, "mdi:flash", None], + "input.transfer.low": ["Low Voltage Transfer", VOLT, None, DEVICE_CLASS_VOLTAGE], + "input.transfer.high": ["High Voltage Transfer", VOLT, None, DEVICE_CLASS_VOLTAGE], "input.transfer.reason": [ "Voltage Transfer Reason", "", "mdi:information-outline", None, ], - "input.voltage": ["Input Voltage", VOLT, "mdi:flash", None], - "input.voltage.nominal": ["Nominal Input Voltage", VOLT, "mdi:flash", None], + "input.voltage": ["Input Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], + "input.voltage.nominal": [ + "Nominal Input Voltage", + VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], "input.frequency": ["Input Line Frequency", FREQUENCY_HERTZ, "mdi:flash", None], "input.frequency.nominal": [ "Nominal Input Line Frequency", @@ -207,8 +219,13 @@ SENSOR_TYPES = { "mdi:flash", None, ], - "output.voltage": ["Output Voltage", VOLT, "mdi:flash", None], - "output.voltage.nominal": ["Nominal Output Voltage", VOLT, "mdi:flash", None], + "output.voltage": ["Output Voltage", VOLT, None, DEVICE_CLASS_VOLTAGE], + "output.voltage.nominal": [ + "Nominal Output Voltage", + VOLT, + None, + DEVICE_CLASS_VOLTAGE, + ], "output.frequency": ["Output Frequency", FREQUENCY_HERTZ, "mdi:flash", None], "output.frequency.nominal": [ "Nominal Output Frequency", @@ -216,6 +233,18 @@ SENSOR_TYPES = { "mdi:flash", None, ], + "ambient.humidity": [ + "Ambient Humidity", + PERCENTAGE, + None, + DEVICE_CLASS_HUMIDITY, + ], + "ambient.temperature": [ + "Ambient Temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], } STATE_TYPES = { diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 174405e22e2..2e3826935fe 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,6 +1,7 @@ """Provides a sensor to track various status aspects of a UPS.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_STATE, CONF_RESOURCES, STATE_UNKNOWN from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -76,7 +77,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NUTSensor(CoordinatorEntity): +class NUTSensor(CoordinatorEntity, SensorEntity): """Representation of a sensor entity for NUT status values.""" def __init__( @@ -160,7 +161,7 @@ class NUTSensor(CoordinatorEntity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the sensor attributes.""" return {ATTR_STATE: _format_display_state(self._data.status)} diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index 1b71280b6a9..97e637fdcb3 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -41,6 +41,10 @@ "scan_interval": "Scan Interval (seconds)" } } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/nut/translations/ca.json b/homeassistant/components/nut/translations/ca.json index fa7b728e115..fc8b5b719a0 100644 --- a/homeassistant/components/nut/translations/ca.json +++ b/homeassistant/components/nut/translations/ca.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/de.json b/homeassistant/components/nut/translations/de.json index 50d37fa8ec4..990f21523b6 100644 --- a/homeassistant/components/nut/translations/de.json +++ b/homeassistant/components/nut/translations/de.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json index 2e5db79d81c..3d57189f7a5 100644 --- a/homeassistant/components/nut/translations/en.json +++ b/homeassistant/components/nut/translations/en.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/et.json b/homeassistant/components/nut/translations/et.json index 27745bfeeae..83d793eb22c 100644 --- a/homeassistant/components/nut/translations/et.json +++ b/homeassistant/components/nut/translations/et.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "\u00dchendus nurjus", + "unknown": "Tundmatu viga" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/fr.json b/homeassistant/components/nut/translations/fr.json index d91bd1e4070..35739689425 100644 --- a/homeassistant/components/nut/translations/fr.json +++ b/homeassistant/components/nut/translations/fr.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/he.json b/homeassistant/components/nut/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/nut/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json index 1ca56c7684f..a7bad455dc3 100644 --- a/homeassistant/components/nut/translations/hu.json +++ b/homeassistant/components/nut/translations/hu.json @@ -1,5 +1,12 @@ { "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" + }, "step": { "user": { "data": { @@ -10,5 +17,11 @@ } } } + }, + "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/id.json b/homeassistant/components/nut/translations/id.json new file mode 100644 index 00000000000..fc23e34fe8e --- /dev/null +++ b/homeassistant/components/nut/translations/id.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "resources": { + "data": { + "resources": "Sumber Daya" + }, + "title": "Pilih Sumber Daya untuk Dipantau" + }, + "ups": { + "data": { + "alias": "Alias", + "resources": "Sumber Daya" + }, + "title": "Pilih UPS untuk Dipantau" + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke server NUT" + } + } + }, + "options": { + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "init": { + "data": { + "resources": "Sumber Daya", + "scan_interval": "Interval Pindai (detik)" + }, + "description": "Pilih Sumber Daya Sensor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/it.json b/homeassistant/components/nut/translations/it.json index 440cb421504..cb8949fca64 100644 --- a/homeassistant/components/nut/translations/it.json +++ b/homeassistant/components/nut/translations/it.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/ko.json b/homeassistant/components/nut/translations/ko.json index 0fb8339ddfc..a5680f4ed48 100644 --- a/homeassistant/components/nut/translations/ko.json +++ b/homeassistant/components/nut/translations/ko.json @@ -33,13 +33,17 @@ } }, "options": { + "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": { "init": { "data": { "resources": "\ub9ac\uc18c\uc2a4", "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" }, - "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4 \uc120\ud0dd" + "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/nut/translations/nl.json b/homeassistant/components/nut/translations/nl.json index 5e4acf3574d..d90b75b4bcc 100644 --- a/homeassistant/components/nut/translations/nl.json +++ b/homeassistant/components/nut/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Verbinden mislukt", + "unknown": "Onverwachte fout" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json index 0af64ad4fa4..887d3b6d30a 100644 --- a/homeassistant/components/nut/translations/no.json +++ b/homeassistant/components/nut/translations/no.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json index 240b32bfe19..2686a98e697 100644 --- a/homeassistant/components/nut/translations/pl.json +++ b/homeassistant/components/nut/translations/pl.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/pt.json b/homeassistant/components/nut/translations/pt.json index a856ef0aeed..f5e8690e383 100644 --- a/homeassistant/components/nut/translations/pt.json +++ b/homeassistant/components/nut/translations/pt.json @@ -22,5 +22,11 @@ } } } + }, + "options": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/ru.json b/homeassistant/components/nut/translations/ru.json index 7a3f1c9b47e..071a7d0f09c 100644 --- a/homeassistant/components/nut/translations/ru.json +++ b/homeassistant/components/nut/translations/ru.json @@ -26,13 +26,17 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 \u0441\u0435\u0440\u0432\u0435\u0440\u0443 NUT" } } }, "options": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/zh-Hans.json b/homeassistant/components/nut/translations/zh-Hans.json index a5f4ff11f09..91522c7f609 100644 --- a/homeassistant/components/nut/translations/zh-Hans.json +++ b/homeassistant/components/nut/translations/zh-Hans.json @@ -1,11 +1,22 @@ { "config": { "step": { + "resources": { + "data": { + "resources": "\u8d44\u6e90" + } + }, "user": { "data": { "username": "\u7528\u6237\u540d" } } } + }, + "options": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "unknown": "\u4e0d\u5728\u9884\u671f\u5185\u7684\u9519\u8bef" + } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index 7c65e836f9e..822d2e785f2 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index a0958be8d9e..569a8adf83b 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -1,8 +1,10 @@ """The National Weather Service integration.""" +from __future__ import annotations + import asyncio import datetime import logging -from typing import Awaitable, Callable, Optional +from typing import Awaitable, Callable from pynws import SimpleNWS @@ -58,8 +60,8 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator): name: str, update_interval: datetime.timedelta, failed_update_interval: datetime.timedelta, - update_method: Optional[Callable[[], Awaitable]] = None, - request_refresh_debouncer: Optional[debounce.Debouncer] = None, + update_method: Callable[[], Awaitable] | None = None, + request_refresh_debouncer: debounce.Debouncer | None = None, ): """Initialize NWS coordinator.""" super().__init__( @@ -157,9 +159,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -169,8 +171,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py index 12ab09abaae..cfe43f3a528 100644 --- a/homeassistant/components/nws/config_flow.py +++ b/homeassistant/components/nws/config_flow.py @@ -11,7 +11,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import base_unique_id -from .const import CONF_STATION, DOMAIN # pylint:disable=unused-import +from .const import CONF_STATION, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nws/translations/he.json b/homeassistant/components/nws/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/nws/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/hu.json b/homeassistant/components/nws/translations/hu.json index 5f4f7bb8bee..1d674cacc7e 100644 --- a/homeassistant/components/nws/translations/hu.json +++ b/homeassistant/components/nws/translations/hu.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "user": { "data": { - "api_key": "API kulcs" + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" } } } diff --git a/homeassistant/components/nws/translations/id.json b/homeassistant/components/nws/translations/id.json new file mode 100644 index 00000000000..3c92ebaed64 --- /dev/null +++ b/homeassistant/components/nws/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "longitude": "Bujur", + "station": "Kode stasiun METAR" + }, + "description": "Jika kode stasiun METAR tidak ditentukan, informasi lintang dan bujur akan digunakan untuk menemukan stasiun terdekat. Untuk saat ini, Kunci API bisa berupa nilai sebarang. Disarankan untuk menggunakan alamat email yang valid.", + "title": "Hubungkan ke National Weather Service" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/ko.json b/homeassistant/components/nws/translations/ko.json index 9fbdf026558..6cf4ca0c9c5 100644 --- a/homeassistant/components/nws/translations/ko.json +++ b/homeassistant/components/nws/translations/ko.json @@ -15,7 +15,7 @@ "longitude": "\uacbd\ub3c4", "station": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc" }, - "description": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc\uac00 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uac00\uc7a5 \uac00\uae4c\uc6b4 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ud604\uc7ac API Key \ub294 \uc544\ubb34 \ud0a4\ub098 \ub123\uc5b4\ub3c4 \uc0c1\uad00 \uc5c6\uc2b5\ub2c8\ub2e4\ub9cc, \uc62c\ubc14\ub978 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uad8c\uc7a5\ud569\ub2c8\ub2e4.", + "description": "METAR \uc2a4\ud14c\uc774\uc158 \ucf54\ub4dc\uac00 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uac00\uc7a5 \uac00\uae4c\uc6b4 \uc2a4\ud14c\uc774\uc158\uc744 \ucc3e\ub294 \ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \ud604\uc7ac API Key\ub294 \uc544\ubb34 \ud0a4\ub098 \ub123\uc5b4\ub3c4 \uc0c1\uad00\uc5c6\uc2b5\ub2c8\ub2e4\ub9cc, \uc62c\ubc14\ub978 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4.", "title": "\ubbf8\uad6d \uae30\uc0c1\uccad\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/nws/translations/nl.json b/homeassistant/components/nws/translations/nl.json index b74e6db96a2..5332f43f4c7 100644 --- a/homeassistant/components/nws/translations/nl.json +++ b/homeassistant/components/nws/translations/nl.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Service is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "api_key": "API-sleutel (e-mail)", + "api_key": "API-sleutel", "latitude": "Breedtegraad", "longitude": "Lengtegraad", "station": "METAR-zendercode" }, - "description": "Als er geen METAR-zendercode is opgegeven, worden de lengte- en breedtegraad gebruikt om het dichtstbijzijnde station te vinden.", + "description": "Als er geen METAR-stationscode is opgegeven, worden de lengte- en breedtegraad gebruikt om het dichtstbijzijnde station te vinden. Voorlopig kan een API-sleutel van alles zijn. Het wordt aanbevolen om een geldig e-mailadres te gebruiken.", "title": "Maak verbinding met de National Weather Service" } } diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 2db3531f879..058ac6c5795 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -105,7 +105,7 @@ class NX584ZoneSensor(BinarySensorEntity): return self._zone["state"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"zone_number": self._zone["number"]} diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index eba5eb58baf..48abe597f5a 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -95,10 +94,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool options=entry.options, ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() undo_listener = entry.add_update_listener(_async_update_listener) @@ -107,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) _async_register_services(hass, coordinator) @@ -122,8 +118,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index f593eeb0729..a352c4df6ed 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -1,6 +1,8 @@ """Config flow for NZBGet.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any import voluptuous as vol @@ -24,14 +26,14 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DOMAIN, ) -from .const import DOMAIN # pylint: disable=unused-import from .coordinator import NZBGetAPI, NZBGetAPIException _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: +def validate_input(hass: HomeAssistantType, 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. @@ -63,8 +65,8 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): return NZBGetOptionsFlowHandler(config_entry) async def async_step_import( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """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 @@ -72,8 +74,8 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input) async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -127,7 +129,7 @@ class NZBGetOptionsFlowHandler(OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: Optional[ConfigType] = None): + async def async_step_init(self, user_input: ConfigType | None = None): """Manage NZBGet options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index b4133e7550d..54a88c89f53 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,8 +1,11 @@ """Monitor the NZBGet API.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Callable, List, Optional +from typing import Callable +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, @@ -41,7 +44,7 @@ SENSOR_TYPES = { async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up NZBGet sensor based on a config entry.""" coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ @@ -64,7 +67,7 @@ async def async_setup_entry( async_add_entities(sensors) -class NZBGetSensor(NZBGetEntity): +class NZBGetSensor(NZBGetEntity, SensorEntity): """Representation of a NZBGet sensor.""" def __init__( @@ -74,7 +77,7 @@ class NZBGetSensor(NZBGetEntity): entry_name: str, sensor_type: str, sensor_name: str, - unit_of_measurement: Optional[str] = None, + unit_of_measurement: str | None = None, ): """Initialize a new NZBGet sensor.""" self._sensor_type = sensor_type diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index c4ceaab5ded..4f0eae17c23 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -1,5 +1,7 @@ """Support for NZBGet switches.""" -from typing import Callable, List +from __future__ import annotations + +from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -15,7 +17,7 @@ from .coordinator import NZBGetDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up NZBGet sensor based on a config entry.""" coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ diff --git a/homeassistant/components/nzbget/translations/hu.json b/homeassistant/components/nzbget/translations/hu.json new file mode 100644 index 00000000000..9eee6cc3be6 --- /dev/null +++ b/homeassistant/components/nzbget/translations/hu.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Hoszt", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "title": "Csatlakoz\u00e1s az NZBGet-hez" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (m\u00e1sodpercben)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/id.json b/homeassistant/components/nzbget/translations/id.json new file mode 100644 index 00000000000..af096f4ef5f --- /dev/null +++ b/homeassistant/components/nzbget/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "title": "Hubungkan ke NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frekuensi pembaruan (dalam detik)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/ko.json b/homeassistant/components/nzbget/translations/ko.json index 58d38b66361..0de5bbb3dd8 100644 --- a/homeassistant/components/nzbget/translations/ko.json +++ b/homeassistant/components/nzbget/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -19,7 +19,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, - "title": "NZBGet\uc5d0 \uc5f0\uacb0" + "title": "NZBGet\uc5d0 \uc5f0\uacb0\ud558\uae30" } } }, @@ -27,7 +27,7 @@ "step": { "init": { "data": { - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08)" + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4 (\ucd08)" } } } diff --git a/homeassistant/components/nzbget/translations/ru.json b/homeassistant/components/nzbget/translations/ru.json index e4f0a44fbcc..4c5d7379527 100644 --- a/homeassistant/components/nzbget/translations/ru.json +++ b/homeassistant/components/nzbget/translations/ru.json @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "title": "NZBGet" diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 4bf6b395d5f..71af8dacba2 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -6,10 +6,9 @@ from operator import itemgetter import oasatelematics import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([OASATelematicsSensor(data, stop_id, route_id, name)], True) -class OASATelematicsSensor(Entity): +class OASATelematicsSensor(SensorEntity): """Implementation of the OASA Telematics sensor.""" def __init__(self, data, stop_id, route_id, name): @@ -79,7 +78,7 @@ class OASATelematicsSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" params = {} if self._times is not None: diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index c105b91971d..639b9eb332f 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -5,7 +5,7 @@ import logging from pyobihai import PyObihai import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -13,7 +13,6 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -69,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class ObihaiServiceSensors(Entity): +class ObihaiServiceSensors(SensorEntity): """Get the status of each Obihai Lines.""" def __init__(self, pyobihai, serial, service_name): @@ -147,9 +146,8 @@ class ObihaiServiceSensors(Entity): services = self._pyobihai.get_line_state() - if services is not None: - if self._service_name in services: - self._state = services.get(self._service_name) + if services is not None and self._service_name in services: + self._state = services.get(self._service_name) call_direction = self._pyobihai.get_call_direction() diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 6f178f26578..918f0258f78 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -6,7 +6,6 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from homeassistant.components.discovery import SERVICE_OCTOPRINT from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, @@ -22,7 +21,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_SECONDS, ) -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import slugify as util_slugify @@ -132,12 +130,6 @@ def setup(hass, config): printers = hass.data[DOMAIN] = {} success = False - def device_discovered(service, info): - """Get called when an Octoprint server has been discovered.""" - _LOGGER.debug("Found an Octoprint server: %s", info) - - discovery.listen(hass, SERVICE_OCTOPRINT, device_discovered) - if DOMAIN not in config: # Skip the setup if there is no configuration present return True @@ -220,14 +212,12 @@ class OctoPrintAPI: now = time.time() if endpoint == "job": last_time = self.job_last_reading[1] - if last_time is not None: - if now - last_time < 30.0: - return self.job_last_reading[0] + if last_time is not None and now - last_time < 30.0: + return self.job_last_reading[0] elif endpoint == "printer": last_time = self.printer_last_reading[1] - if last_time is not None: - if now - last_time < 30.0: - return self.printer_last_reading[0] + if last_time is not None and now - last_time < 30.0: + return self.printer_last_reading[0] url = self.api_url + endpoint try: @@ -308,8 +298,7 @@ def get_value_from_json(json_dict, sensor_type, group, tool): return json_dict[group][sensor_type] - if tool is not None: - if sensor_type in json_dict[group][tool]: - return json_dict[group][tool][sensor_type] + if tool is not None and sensor_type in json_dict[group][tool]: + return json_dict[group][tool][sensor_type] return None diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 921f355edbe..16f6efce004 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -3,8 +3,8 @@ import logging import requests +from homeassistant.components.sensor import SensorEntity from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from homeassistant.helpers.entity import Entity from . import DOMAIN as COMPONENT_DOMAIN, SENSOR_TYPES @@ -25,18 +25,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] tools = octoprint_api.get_tools() - if "Temperatures" in monitored_conditions: - if not tools: - hass.components.persistent_notification.create( - "Your printer appears to be offline.
" - "If you do not want to have your printer on
" - " at all times, and you would like to monitor
" - "temperatures, please add
" - "bed and/or number_of_tools to your configuration
" - "and restart.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) + if "Temperatures" in monitored_conditions and not tools: + hass.components.persistent_notification.create( + "Your printer appears to be offline.
" + "If you do not want to have your printer on
" + " at all times, and you would like to monitor
" + "temperatures, please add
" + "bed and/or number_of_tools to your configuration
" + "and restart.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) devices = [] types = ["actual", "target"] @@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class OctoPrintSensor(Entity): +class OctoPrintSensor(SensorEntity): """Representation of an OctoPrint sensor.""" def __init__( diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 7c7331990ea..b53c35e17b5 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -6,10 +6,9 @@ import defusedxml.ElementTree as ET import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -34,7 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([OhmconnectSensor(name, ohmid)], True) -class OhmconnectSensor(Entity): +class OhmconnectSensor(SensorEntity): """Representation of a OhmConnect sensor.""" def __init__(self, name, ohmid): @@ -56,7 +55,7 @@ class OhmconnectSensor(Entity): return "Inactive" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"Address": self._data.get("address"), "ID": self._ohmid} diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index dcd8f264161..5db46658eb1 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -5,6 +5,7 @@ import pyombi import voluptuous as vol from homeassistant.const import ( + ATTR_NAME, CONF_API_KEY, CONF_HOST, CONF_PASSWORD, @@ -15,7 +16,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from .const import ( - ATTR_NAME, ATTR_SEASON, CONF_URLBASE, DEFAULT_PORT, diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py index 42b58e7f50d..784b46a99b7 100644 --- a/homeassistant/components/ombi/const.py +++ b/homeassistant/components/ombi/const.py @@ -1,5 +1,4 @@ """Support for Ombi.""" -ATTR_NAME = "name" ATTR_SEASON = "season" CONF_URLBASE = "urlbase" diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index 2a2f50532b4..8c08b026b28 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -4,7 +4,7 @@ import logging from pyombi import OmbiError -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from .const import DOMAIN, SENSOR_TYPES @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class OmbiSensor(Entity): +class OmbiSensor(SensorEntity): """Representation of an Ombi sensor.""" def __init__(self, label, sensor_type, ombi, icon): diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index ff4dd93a0e1..e5a545e4806 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -56,19 +56,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name="Omnilogic", polling_interval=polling_interval, ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, OMNI_API: api, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -79,8 +76,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 791d81b6757..6f7ee6e5eb5 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -141,7 +141,7 @@ class OmniLogicEntity(CoordinatorEntity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes.""" return self._attrs diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py index 641ec5a8d94..f8dffaeda44 100644 --- a/homeassistant/components/omnilogic/config_flow.py +++ b/homeassistant/components/omnilogic/config_flow.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import CONF_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import +from .const import CONF_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json index d999a34f076..2b2a4a9fe3d 100644 --- a/homeassistant/components/omnilogic/manifest.json +++ b/homeassistant/components/omnilogic/manifest.json @@ -3,6 +3,6 @@ "name": "Hayward Omnilogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/omnilogic", - "requirements": ["omnilogic==0.4.2"], + "requirements": ["omnilogic==0.4.3"], "codeowners": ["@oliver84","@djtimca","@gentoosu"] } diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index e1b4b387a46..25457224e9f 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -1,5 +1,5 @@ """Definition and setup of the Omnilogic Sensors for Home Assistant.""" -from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, MASS_GRAMS, @@ -59,7 +59,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class OmnilogicSensor(OmniLogicEntity): +class OmnilogicSensor(OmniLogicEntity, SensorEntity): """Defines an Omnilogic sensor entity.""" def __init__( diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 4378d39912d..85de80d3dfa 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Abfrageintervall (in Sekunden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/hu.json b/homeassistant/components/omnilogic/translations/hu.json new file mode 100644 index 00000000000..129bb041b42 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (m\u00e1sodpercben)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/id.json b/homeassistant/components/omnilogic/translations/id.json new file mode 100644 index 00000000000..ed19cc68cf8 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Interval polling (dalam detik)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/ko.json b/homeassistant/components/omnilogic/translations/ko.json index 74786104624..0f3df64c00f 100644 --- a/homeassistant/components/omnilogic/translations/ko.json +++ b/homeassistant/components/omnilogic/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", @@ -21,7 +21,7 @@ "step": { "init": { "data": { - "polling_interval": "\ud3f4\ub9c1 \uac04\uaca9(\ucd08)" + "polling_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ucd08)" } } } diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 828f0530830..5b00efefa1a 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index bedfa703a9b..e383e4e32c4 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -4,10 +4,17 @@ from homeassistant.helpers.storage import Store from homeassistant.loader import bind_hass from . import views -from .const import DOMAIN, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, STEPS +from .const import ( + DOMAIN, + STEP_ANALYTICS, + STEP_CORE_CONFIG, + STEP_INTEGRATION, + STEP_USER, + STEPS, +) STORAGE_KEY = DOMAIN -STORAGE_VERSION = 3 +STORAGE_VERSION = 4 class OnboadingStorage(Store): @@ -20,6 +27,8 @@ class OnboadingStorage(Store): old_data["done"].append(STEP_INTEGRATION) if old_version < 3: old_data["done"].append(STEP_CORE_CONFIG) + if old_version < 4: + old_data["done"].append(STEP_ANALYTICS) return old_data diff --git a/homeassistant/components/onboarding/const.py b/homeassistant/components/onboarding/const.py index bf350a200de..5a771f524ac 100644 --- a/homeassistant/components/onboarding/const.py +++ b/homeassistant/components/onboarding/const.py @@ -3,7 +3,8 @@ DOMAIN = "onboarding" STEP_USER = "user" STEP_CORE_CONFIG = "core_config" STEP_INTEGRATION = "integration" +STEP_ANALYTICS = "analytics" -STEPS = [STEP_USER, STEP_CORE_CONFIG, STEP_INTEGRATION] +STEPS = [STEP_USER, STEP_CORE_CONFIG, STEP_ANALYTICS, STEP_INTEGRATION] DEFAULT_AREAS = ("living_room", "kitchen", "bedroom") diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index e2fb8e084b8..06c9946b5c9 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -6,6 +6,7 @@ "hassio" ], "dependencies": [ + "analytics", "auth", "http", "person" diff --git a/homeassistant/components/onboarding/translations/id.json b/homeassistant/components/onboarding/translations/id.json index 33e8a88a9ae..472630cacf6 100644 --- a/homeassistant/components/onboarding/translations/id.json +++ b/homeassistant/components/onboarding/translations/id.json @@ -1,5 +1,7 @@ { "area": { - "kitchen": "Dapur" + "bedroom": "Kamar Tidur", + "kitchen": "Dapur", + "living_room": "Ruang Keluarga" } } \ No newline at end of file diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 1d5528688dd..dec80642845 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -14,6 +14,7 @@ from homeassistant.core import callback from .const import ( DEFAULT_AREAS, DOMAIN, + STEP_ANALYTICS, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, @@ -27,6 +28,7 @@ async def async_setup(hass, data, store): hass.http.register_view(UserOnboardingView(data, store)) hass.http.register_view(CoreConfigOnboardingView(data, store)) hass.http.register_view(IntegrationOnboardingView(data, store)) + hass.http.register_view(AnalyticsOnboardingView(data, store)) class OnboardingView(HomeAssistantView): @@ -217,6 +219,28 @@ class IntegrationOnboardingView(_BaseOnboardingView): return self.json({"auth_code": auth_code}) +class AnalyticsOnboardingView(_BaseOnboardingView): + """View to finish analytics onboarding step.""" + + url = "/api/onboarding/analytics" + name = "api:onboarding:analytics" + step = STEP_ANALYTICS + + async def post(self, request): + """Handle finishing analytics step.""" + hass = request.app["hass"] + + async with self._lock: + if self._async_is_done(): + return self.json_message( + "Analytics config step already done", HTTP_FORBIDDEN + ) + + await self._async_mark_done(hass) + + return self.json({}) + + @callback def _async_get_hass_provider(hass): """Get the Home Assistant auth provider.""" diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 69538c5e8b3..4dac83815ba 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -35,9 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -48,8 +48,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index b34ee4eae35..3af2bb7c326 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -4,6 +4,7 @@ import logging from ondilo import OndiloError +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, @@ -83,7 +84,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class OndiloICO(CoordinatorEntity): +class OndiloICO(CoordinatorEntity, SensorEntity): """Representation of a Sensor.""" def __init__( diff --git a/homeassistant/components/ondilo_ico/translations/de.json b/homeassistant/components/ondilo_ico/translations/de.json index 5bab6ed132b..ad11cefde66 100644 --- a/homeassistant/components/ondilo_ico/translations/de.json +++ b/homeassistant/components/ondilo_ico/translations/de.json @@ -12,5 +12,6 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } - } + }, + "title": "Ondilo ICO" } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/hu.json b/homeassistant/components/ondilo_ico/translations/hu.json new file mode 100644 index 00000000000..cae1f6d20c0 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/id.json b/homeassistant/components/ondilo_ico/translations/id.json new file mode 100644 index 00000000000..1227a6d6689 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ko.json b/homeassistant/components/ondilo_ico/translations/ko.json index fa000ea1c06..88f3d678171 100644 --- a/homeassistant/components/ondilo_ico/translations/ko.json +++ b/homeassistant/components/ondilo_ico/translations/ko.json @@ -12,5 +12,6 @@ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } - } + }, + "title": "Ondilo ICO" } \ No newline at end of file diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 6d64478aa72..e5a214ce8a4 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,13 +1,17 @@ """The 1-Wire component.""" import asyncio +import logging from homeassistant.config_entries import ConfigEntry 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, SUPPORTED_PLATFORMS +from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Set up 1-Wire integrations.""" @@ -26,10 +30,43 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): hass.data[DOMAIN][config_entry.unique_id] = onewirehub - for component in SUPPORTED_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + async def cleanup_registry() -> None: + # Get registries + device_registry, entity_registry = await asyncio.gather( + hass.helpers.device_registry.async_get_registry(), + hass.helpers.entity_registry.async_get_registry(), ) + # Generate list of all device entries + registry_devices = [ + entry.id + for entry in dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + ] + # Remove devices that don't belong to any entity + for device_id in registry_devices: + if not er.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True + ): + _LOGGER.debug( + "Removing device `%s` because it does not have any entities", + device_id, + ) + device_registry.async_remove_device(device_id) + + async def start_platforms() -> None: + """Start platforms and cleanup devices.""" + # wait until all required platforms are ready + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(config_entry, platform) + for platform in PLATFORMS + ] + ) + await cleanup_registry() + + hass.async_create_task(start_platforms()) + return True @@ -38,8 +75,8 @@ async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry) unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in SUPPORTED_PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 9ad4d5347f0..fbb1d5debef 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -5,7 +5,7 @@ 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 .const import ( # pylint: disable=unused-import +from .const import ( CONF_MOUNT_DIR, CONF_TYPE_OWFS, CONF_TYPE_OWSERVER, diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index e68039078e9..54b18f7c905 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -61,7 +61,7 @@ SENSOR_TYPES = { SWITCH_TYPE_PIO: [None, None], } -SUPPORTED_PLATFORMS = [ +PLATFORMS = [ BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN, diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 9238bb5d32c..10c2b0c24a7 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -1,6 +1,8 @@ """Support for 1-Wire entities.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from pyownet import protocol @@ -43,32 +45,27 @@ class OneWireBaseEntity(Entity): self._unique_id = unique_id or device_file @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device.""" return self._device_class @property - def unit_of_measurement(self) -> Optional[str]: - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return {"device_file": self._device_file, "raw_value": self._value_raw} @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._unique_id @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> dict[str, Any] | None: """Return device specific attributes.""" return self._device_info @@ -85,9 +82,9 @@ class OneWireProxyEntity(OneWireBaseEntity): self, device_id: str, device_name: str, - device_info: Dict[str, Any], + device_info: dict[str, Any], entity_path: str, - entity_specs: Dict[str, Any], + entity_specs: dict[str, Any], owproxy: protocol._Proxy, ): """Initialize the sensor.""" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 4888383fa42..02af7a89ae3 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,4 +1,6 @@ """Support for 1-Wire environment sensors.""" +from __future__ import annotations + from glob import glob import logging import os @@ -6,7 +8,7 @@ import os from pi1wire import InvalidCRCException, UnsupportResponseException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE import homeassistant.helpers.config_validation as cv @@ -264,9 +266,8 @@ def get_entities(onewirehub: OneWireHub, config): """Get a list of entities.""" entities = [] device_names = {} - if CONF_NAMES in config: - if isinstance(config[CONF_NAMES], dict): - device_names = config[CONF_NAMES] + if CONF_NAMES in config and isinstance(config[CONF_NAMES], dict): + device_names = config[CONF_NAMES] conf_type = config[CONF_TYPE] # We have an owserver on a remote(or local) host/port @@ -394,7 +395,16 @@ def get_entities(onewirehub: OneWireHub, config): return entities -class OneWireProxySensor(OneWireProxyEntity): +class OneWireSensor(OneWireBaseEntity, SensorEntity): + """Mixin for sensor specific attributes.""" + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + +class OneWireProxySensor(OneWireProxyEntity, OneWireSensor): """Implementation of a 1-Wire sensor connected through owserver.""" @property @@ -403,7 +413,7 @@ class OneWireProxySensor(OneWireProxyEntity): return self._state -class OneWireDirectSensor(OneWireBaseEntity): +class OneWireDirectSensor(OneWireSensor): """Implementation of a 1-Wire sensor directly connected to RPI GPIO.""" def __init__(self, name, device_file, device_info, owsensor): @@ -431,7 +441,7 @@ class OneWireDirectSensor(OneWireBaseEntity): self._state = value -class OneWireOWFSSensor(OneWireBaseEntity): # pragma: no cover +class OneWireOWFSSensor(OneWireSensor): # pragma: no cover """Implementation of a 1-Wire sensor through owfs. This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index d3ed8137da3..2b0630db22c 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -12,12 +12,14 @@ "data": { "host": "Host", "port": "Port" - } + }, + "title": "owserver-Details einstellen" }, "user": { "data": { "type": "Verbindungstyp" - } + }, + "title": "1-Wire einrichten" } } } diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index 8ac8f6d3b03..662475dde2c 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_path": "A k\u00f6nyvt\u00e1r nem tal\u00e1lhat\u00f3." @@ -7,14 +10,15 @@ "step": { "owserver": { "data": { - "host": "Gazdag\u00e9p", + "host": "Hoszt", "port": "Port" } }, "user": { "data": { "type": "Kapcsolat t\u00edpusa" - } + }, + "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/onewire/translations/id.json b/homeassistant/components/onewire/translations/id.json new file mode 100644 index 00000000000..5de8e2eee3e --- /dev/null +++ b/homeassistant/components/onewire/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_path": "Direktori tidak ditemukan." + }, + "step": { + "owserver": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Tetapkan detail owserver" + }, + "user": { + "data": { + "type": "Jenis koneksi" + }, + "title": "Siapkan 1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ko.json b/homeassistant/components/onewire/translations/ko.json index 871482b766b..038be16108a 100644 --- a/homeassistant/components/onewire/translations/ko.json +++ b/homeassistant/components/onewire/translations/ko.json @@ -4,14 +4,22 @@ "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" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_path": "\ub514\ub809\ud130\ub9ac\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { "owserver": { "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" - } + }, + "title": "owserver \uc138\ubd80 \uc815\ubcf4 \uc124\uc815\ud558\uae30" + }, + "user": { + "data": { + "type": "\uc5f0\uacb0 \uc720\ud615" + }, + "title": "1-Wire \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 7ac9b5fdfc6..2e4b6eff6da 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -1,6 +1,7 @@ """Support for Onkyo Receivers.""" +from __future__ import annotations + import logging -from typing import List import eiscp from eiscp import eISCP @@ -56,7 +57,7 @@ SUPPORT_ONKYO_WO_VOLUME = ( | SUPPORT_PLAY_MEDIA ) -KNOWN_HOSTS: List[str] = [] +KNOWN_HOSTS: list[str] = [] DEFAULT_SOURCES = { "tv": "TV", "bd": "Bluray", @@ -392,7 +393,7 @@ class OnkyoDevice(MediaPlayerEntity): return self._source_list @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return self._attributes diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index b332b7a795a..0eb39064db7 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -88,9 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if device.capabilities.events: platforms += ["binary_sensor", "sensor"] - for component in platforms: + for platform in platforms: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) @@ -111,8 +111,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in platforms + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in platforms ] ) ) diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index 9b5469ee0d0..680f92efd2b 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -1,5 +1,5 @@ """Support for ONVIF binary sensors.""" -from typing import Optional +from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback @@ -55,7 +55,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, BinarySensorEntity): return self.device.events.get_uid(self.uid).name @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return self.device.events.get_uid(self.uid).device_class diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 273640ab6b5..0de93627953 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -1,6 +1,7 @@ """Config flow for ONVIF.""" +from __future__ import annotations + from pprint import pformat -from typing import List from urllib.parse import urlparse from onvif.exceptions import ONVIFError @@ -21,7 +22,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback -# pylint: disable=unused-import from .const import ( CONF_DEVICE_ID, CONF_RTSP_TRANSPORT, @@ -36,7 +36,7 @@ from .device import get_device CONF_MANUAL_INPUT = "Manually configure ONVIF device" -def wsdiscovery() -> List[Service]: +def wsdiscovery() -> list[Service]: """Get ONVIF Profile S devices from network.""" discovery = WSDiscovery(ttl=4) discovery.start() @@ -49,7 +49,7 @@ def wsdiscovery() -> List[Service]: async def async_discovery(hass) -> bool: """Return if there are devices that can be discovered.""" - LOGGER.debug("Starting ONVIF discovery...") + LOGGER.debug("Starting ONVIF discovery") services = await hass.async_add_executor_job(wsdiscovery) devices = [] diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index c0851cbe32f..826ff4b1a29 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -1,8 +1,10 @@ """ONVIF device abstraction.""" +from __future__ import annotations + import asyncio +from contextlib import suppress import datetime as dt import os -from typing import List from httpx import RequestError import onvif @@ -50,7 +52,7 @@ class ONVIFDevice: self.info: DeviceInfo = DeviceInfo() self.capabilities: Capabilities = Capabilities() - self.profiles: List[Profile] = [] + self.profiles: list[Profile] = [] self.max_resolution: int = 0 self._dt_diff_seconds: int = 0 @@ -240,29 +242,23 @@ class ONVIFDevice: async def async_get_capabilities(self): """Obtain information about the available services on the device.""" snapshot = False - try: + with suppress(ONVIFError, Fault, RequestError): media_service = self.device.create_media_service() media_capabilities = await media_service.GetServiceCapabilities() snapshot = media_capabilities and media_capabilities.SnapshotUri - except (ONVIFError, Fault, RequestError): - pass pullpoint = False - try: + with suppress(ONVIFError, Fault, RequestError): pullpoint = await self.events.async_start() - except (ONVIFError, Fault, RequestError): - pass ptz = False - try: + with suppress(ONVIFError, Fault, RequestError): self.device.get_definition("ptz") ptz = True - except (ONVIFError, Fault, RequestError): - pass return Capabilities(snapshot, pullpoint, ptz) - async def async_get_profiles(self) -> List[Profile]: + async def async_get_profiles(self) -> list[Profile]: """Obtain media profiles for this device.""" media_service = self.device.create_media_service() result = await media_service.GetProfiles() @@ -438,7 +434,7 @@ class ONVIFDevice: await ptz_service.Stop(req) except ONVIFError as err: if "Bad Request" in err.reason: - LOGGER.warning("Device '%s' doesn't support PTZ.", self.name) + LOGGER.warning("Device '%s' doesn't support PTZ", self.name) else: LOGGER.error("Error trying to perform PTZ action: %s", err) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index eaf23236042..76b18d729a8 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -1,7 +1,10 @@ """ONVIF event abstraction.""" +from __future__ import annotations + import asyncio +from contextlib import suppress import datetime as dt -from typing import Callable, Dict, List, Optional, Set +from typing import Callable from httpx import RemoteProtocolError, TransportError from onvif import ONVIFCamera, ONVIFService @@ -34,14 +37,14 @@ class EventManager: self.started: bool = False self._subscription: ONVIFService = None - self._events: Dict[str, Event] = {} - self._listeners: List[CALLBACK_TYPE] = [] - self._unsub_refresh: Optional[CALLBACK_TYPE] = None + self._events: dict[str, Event] = {} + self._listeners: list[CALLBACK_TYPE] = [] + self._unsub_refresh: CALLBACK_TYPE | None = None super().__init__() @property - def platforms(self) -> Set[str]: + def platforms(self) -> set[str]: """Return platforms to setup.""" return {event.platform for event in self._events.values()} @@ -84,10 +87,8 @@ class EventManager: # Initialize events pullpoint = self.device.create_pullpoint_service() - try: + with suppress(*SUBSCRIPTION_ERRORS): await pullpoint.SetSynchronizationPoint() - except SUBSCRIPTION_ERRORS: - pass response = await pullpoint.PullMessages( {"MessageLimit": 100, "Timeout": dt.timedelta(seconds=5)} ) @@ -117,10 +118,9 @@ class EventManager: return if self._subscription: - try: + # Suppressed. The subscription may no longer exist. + with suppress(*SUBSCRIPTION_ERRORS): await self._subscription.Unsubscribe() - except SUBSCRIPTION_ERRORS: - pass # Ignored. The subscription may no longer exist. self._subscription = None try: @@ -130,7 +130,7 @@ class EventManager: if not restarted: LOGGER.warning( - "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying...", + "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying", self.unique_id, ) # Try again in a minute @@ -229,6 +229,6 @@ class EventManager: """Retrieve event for given id.""" return self._events[uid] - def get_platform(self, platform) -> List[Event]: + def get_platform(self, platform) -> list[Event]: """Retrieve events for given platform.""" return [event for event in self._events.values() if event.platform == platform] diff --git a/homeassistant/components/onvif/models.py b/homeassistant/components/onvif/models.py index 2a129d3bc44..feda891f772 100644 --- a/homeassistant/components/onvif/models.py +++ b/homeassistant/components/onvif/models.py @@ -1,6 +1,8 @@ """ONVIF models.""" +from __future__ import annotations + from dataclasses import dataclass -from typing import Any, List +from typing import Any @dataclass @@ -37,7 +39,7 @@ class PTZ: continuous: bool relative: bool absolute: bool - presets: List[str] = None + presets: list[str] = None @dataclass diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index cad2b3c8cab..9574d44edea 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -387,3 +387,25 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event: ) except (AttributeError, KeyError, ValueError): return None + + +@PARSERS.register("tns1:RecordingConfig/JobState") +# pylint: disable=protected-access +async def async_parse_jobstate(uid: str, msg) -> Event: + """Handle parsing event message. + + Topic: tns1:RecordingConfig/JobState* + """ + + try: + source = msg.Message._value_1.Source.SimpleItem[0].Value + return Event( + f"{uid}_{msg.Topic._value_1}_{source}", + f"{source} JobState", + "binary_sensor", + None, + None, + msg.Message._value_1.Data.SimpleItem[0].Value == "Active", + ) + except (AttributeError, KeyError): + return None diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index b1d7ff7986c..1c5766e3969 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -1,6 +1,7 @@ """Support for ONVIF binary sensors.""" -from typing import Optional, Union +from __future__ import annotations +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from .base import ONVIFBaseEntity @@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return True -class ONVIFSensor(ONVIFBaseEntity): +class ONVIFSensor(ONVIFBaseEntity, SensorEntity): """Representation of a ONVIF sensor event.""" def __init__(self, uid, device): @@ -43,7 +44,7 @@ class ONVIFSensor(ONVIFBaseEntity): super().__init__(device) @property - def state(self) -> Union[None, str, int, float]: + def state(self) -> None | str | int | float: """Return the state of the entity.""" return self.device.events.get_uid(self.uid).value @@ -53,12 +54,12 @@ class ONVIFSensor(ONVIFBaseEntity): return self.device.events.get_uid(self.uid).name @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return self.device.events.get_uid(self.uid).device_class @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return self.device.events.get_uid(self.uid).unit_of_measurement diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index a3203bfedb3..ecfa1afaab2 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -1,6 +1,12 @@ { "config": { "step": { + "auth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "configure_profile": { "data": { "include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4" diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index c9c3f137984..61a6cfb056e 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "no_h264": "Nem voltak el\u00e9rhet\u0151 H264 streamek. Ellen\u0151rizd a profil konfigur\u00e1ci\u00f3j\u00e1t a k\u00e9sz\u00fcl\u00e9ken.", + "no_mac": "Nem siker\u00fclt konfigur\u00e1lni az egyedi azonos\u00edt\u00f3t az ONVIF eszk\u00f6zh\u00f6z.", + "onvif_error": "Hiba t\u00f6rt\u00e9nt az ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sakor. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizd a napl\u00f3kat." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, @@ -8,7 +15,8 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + }, + "title": "Hiteles\u00edt\u00e9s konfigur\u00e1l\u00e1sa" }, "configure_profile": { "data": { @@ -17,11 +25,19 @@ "description": "L\u00e9trehozza a(z) {profile} f\u00e9nyk\u00e9pez\u0151g\u00e9p entit\u00e1s\u00e1t {resolution} felbont\u00e1ssal?", "title": "Profilok konfigur\u00e1l\u00e1sa" }, + "device": { + "data": { + "host": "V\u00e1laszd ki a felfedezett ONVIF eszk\u00f6zt" + }, + "title": "ONVIF eszk\u00f6z kiv\u00e1laszt\u00e1sa" + }, "manual_input": { "data": { "host": "Hoszt", + "name": "N\u00e9v", "port": "Port" - } + }, + "title": "ONVIF eszk\u00f6z konfigur\u00e1l\u00e1sa" }, "user": { "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban." diff --git a/homeassistant/components/onvif/translations/id.json b/homeassistant/components/onvif/translations/id.json new file mode 100644 index 00000000000..3ed50ae63c4 --- /dev/null +++ b/homeassistant/components/onvif/translations/id.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_h264": "Tidak ada aliran H264 yang tersedia. Periksa konfigurasi profil di perangkat Anda.", + "no_mac": "Tidak dapat mengonfigurasi ID unik untuk perangkat ONVIF.", + "onvif_error": "Terjadi kesalahan saat menyiapkan perangkat ONVIF. Periksa log untuk informasi lebih lanjut." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "auth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Konfigurasikan autentikasi" + }, + "configure_profile": { + "data": { + "include": "Buat entitas kamera" + }, + "description": "Buat entitas kamera untuk {profile} dengan {resolution}?", + "title": "Konfigurasikan Profil" + }, + "device": { + "data": { + "host": "Pilih perangkat ONVIF yang ditemukan" + }, + "title": "Pilih perangkat ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "name": "Nama", + "port": "Port" + }, + "title": "Konfigurasikan perangkat ONVIF" + }, + "user": { + "description": "Dengan mengklik kirim, kami akan mencari perangkat ONVIF pada jaringan Anda yang mendukung Profil S.\n\nBeberapa produsen mulai menonaktifkan ONVIF secara default. Pastikan ONVIF diaktifkan dalam konfigurasi kamera Anda.", + "title": "Penyiapan perangkat ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argumen FFMPEG ekstra", + "rtsp_transport": "Mekanisme transport RTSP" + }, + "title": "Opsi Perangkat ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json index 4d76e939af0..e1fdac8256e 100644 --- a/homeassistant/components/onvif/translations/nl.json +++ b/homeassistant/components/onvif/translations/nl.json @@ -52,7 +52,7 @@ "extra_arguments": "Extra FFMPEG argumenten", "rtsp_transport": "RTSP-transportmechanisme" }, - "title": "[%%] Apparaatopties" + "title": "ONVIF-apparaatopties" } } } diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json index 823dd8eb7fd..6b486bb62e2 100644 --- a/homeassistant/components/onvif/translations/ru.json +++ b/homeassistant/components/onvif/translations/ru.json @@ -14,7 +14,7 @@ "auth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" }, diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index ee913070a11..d098edba5b2 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -99,7 +99,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): return "alpr" @property - def state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index 028d6eacf24..bf63ec0bfff 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -152,7 +152,7 @@ class OpenCVImageProcessor(ImageProcessingEntity): return self._total_matches @property - def state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return {ATTR_MATCHES: self._matches, ATTR_TOTAL_MATCHES: self._total_matches} diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 24b84e305e7..a0294a7aa49 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,6 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.19.2", "opencv-python-headless==4.3.0.36"], + "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], "codeowners": [] } diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index 9a5bf3a9813..33305b677de 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -4,9 +4,9 @@ from datetime import timedelta from openerz_api.main import OpenERZConnector import voluptuous as vol +from homeassistant.components.sensor import SensorEntity import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity SCAN_INTERVAL = timedelta(hours=12) @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([OpenERZSensor(api_connector, config.get(CONF_NAME))], True) -class OpenERZSensor(Entity): +class OpenERZSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, api_connector, name): diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index e0f21f6946d..d7d4149e26d 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -5,7 +5,7 @@ import openevsewifi from requests import RequestException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -14,7 +14,6 @@ from homeassistant.const import ( TIME_MINUTES, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class OpenEVSESensor(Entity): +class OpenEVSESensor(SensorEntity): """Implementation of an OpenEVSE sensor.""" def __init__(self, sensor_type, charger): diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 9846e305291..8474cdab131 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -15,7 +15,6 @@ from homeassistant.const import ( HTTP_OK, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -58,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([OpenexchangeratesSensor(rest, name, quote)], True) -class OpenexchangeratesSensor(Entity): +class OpenexchangeratesSensor(SensorEntity): """Representation of an Open Exchange Rates sensor.""" def __init__(self, rest, name, quote): @@ -79,7 +78,7 @@ class OpenexchangeratesSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other attributes of the sensor.""" attr = self.rest.data attr[ATTR_ATTRIBUTION] = ATTRIBUTION diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index cf6825c867b..154cb4df3ae 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -92,7 +92,7 @@ class OpenGarageCover(CoverEntity): self._open_garage = open_garage self._state = None self._state_before_move = None - self._device_state_attributes = {} + self._extra_state_attributes = {} self._available = True self._device_id = device_id @@ -107,9 +107,9 @@ class OpenGarageCover(CoverEntity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - return self._device_state_attributes + return self._extra_state_attributes @property def is_closed(self): @@ -154,11 +154,11 @@ class OpenGarageCover(CoverEntity): _LOGGER.debug("%s status: %s", self._name, self._state) if status.get("rssi") is not None: - self._device_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi") + self._extra_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi") if status.get("dist") is not None: - self._device_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist") + self._extra_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist") if self._state is not None: - self._device_state_attributes[ATTR_DOOR_STATE] = self._state + self._extra_state_attributes[ATTR_DOOR_STATE] = self._state self._available = True diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 115366dac66..70d0d36176c 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -5,11 +5,10 @@ import logging import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import utcnow @@ -44,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(data.devices, True) -class OpenHardwareMonitorDevice(Entity): +class OpenHardwareMonitorDevice(SensorEntity): """Device used to display information from OpenHardwareMonitor.""" def __init__(self, data, name, path, unit_of_measurement): @@ -73,8 +72,8 @@ class OpenHardwareMonitorDevice(Entity): return self.value @property - def state_attributes(self): - """Return the state attributes of the sun.""" + def extra_state_attributes(self): + """Return the state attributes of the entity.""" return self.attributes @classmethod diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index f9dbd4272ba..270eb22ebda 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -139,7 +139,7 @@ class OpenhomeDevice(MediaPlayerEntity): def play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - if not media_type == MEDIA_TYPE_MUSIC: + if media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 06132e83e88..122388b85b7 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -17,7 +17,6 @@ from homeassistant.const import ( LENGTH_METERS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import distance as util_distance, location as util_location CONF_ALTITUDE = "altitude" @@ -87,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class OpenSkySensor(Entity): +class OpenSkySensor(SensorEntity): """Open Sky Network Sensor.""" def __init__(self, hass, name, latitude, longitude, radius, altitude): @@ -174,7 +173,7 @@ class OpenSkySensor(Entity): self._previously_tracked = currently_tracked @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION} diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index cc08ec3da69..8686997e748 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -39,6 +39,8 @@ from .const import ( CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, + CONF_READ_PRECISION, + CONF_SET_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW, DOMAIN, @@ -94,6 +96,17 @@ async def async_setup_entry(hass, config_entry): gateway = OpenThermGatewayDevice(hass, config_entry) hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway + if config_entry.options.get(CONF_PRECISION): + migrate_options = dict(config_entry.options) + migrate_options.update( + { + CONF_READ_PRECISION: config_entry.options[CONF_PRECISION], + CONF_SET_PRECISION: config_entry.options[CONF_PRECISION], + } + ) + del migrate_options[CONF_PRECISION] + hass.config_entries.async_update_entry(config_entry, options=migrate_options) + config_entry.add_update_listener(options_updated) # Schedule directly on the loop to avoid blocking HA startup. diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 8ec536e7331..294088ee608 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -28,7 +28,14 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id from . import DOMAIN -from .const import CONF_FLOOR_TEMP, CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW +from .const import ( + CONF_FLOOR_TEMP, + CONF_READ_PRECISION, + CONF_SET_PRECISION, + CONF_TEMPORARY_OVRD_MODE, + DATA_GATEWAYS, + DATA_OPENTHERM_GW, +) _LOGGER = logging.getLogger(__name__) @@ -61,7 +68,9 @@ class OpenThermClimate(ClimateEntity): ) self.friendly_name = gw_dev.name self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP) - self.temp_precision = options.get(CONF_PRECISION) + self.temp_read_precision = options.get(CONF_READ_PRECISION) + self.temp_set_precision = options.get(CONF_SET_PRECISION) + self.temporary_ovrd_mode = options.get(CONF_TEMPORARY_OVRD_MODE, True) self._available = False self._current_operation = None self._current_temperature = None @@ -79,7 +88,9 @@ class OpenThermClimate(ClimateEntity): def update_options(self, entry): """Update climate entity options.""" self.floor_temp = entry.options[CONF_FLOOR_TEMP] - self.temp_precision = entry.options[CONF_PRECISION] + self.temp_read_precision = entry.options[CONF_READ_PRECISION] + self.temp_set_precision = entry.options[CONF_SET_PRECISION] + self.temporary_ovrd_mode = entry.options[CONF_TEMPORARY_OVRD_MODE] self.async_write_ha_state() async def async_added_to_hass(self): @@ -178,8 +189,8 @@ class OpenThermClimate(ClimateEntity): @property def precision(self): """Return the precision of the system.""" - if self.temp_precision is not None and self.temp_precision != 0: - return self.temp_precision + if self.temp_read_precision: + return self.temp_read_precision if self.hass.config.units.temperature_unit == TEMP_CELSIUS: return PRECISION_HALVES return PRECISION_WHOLE @@ -234,7 +245,11 @@ class OpenThermClimate(ClimateEntity): @property def target_temperature_step(self): """Return the supported step of target temperature.""" - return self.precision + if self.temp_set_precision: + return self.temp_set_precision + if self.hass.config.units.temperature_unit == TEMP_CELSIUS: + return PRECISION_HALVES + return PRECISION_WHOLE @property def preset_mode(self): @@ -259,7 +274,7 @@ class OpenThermClimate(ClimateEntity): if temp == self.target_temperature: return self._new_target_temperature = await self._gateway.gateway.set_target_temp( - temp + temp, self.temporary_ovrd_mode ) self.async_write_ha_state() diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 8da530bebda..aa764b7ae9e 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -19,7 +19,12 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN -from .const import CONF_FLOOR_TEMP, CONF_PRECISION +from .const import ( + CONF_FLOOR_TEMP, + CONF_READ_PRECISION, + CONF_SET_PRECISION, + CONF_TEMPORARY_OVRD_MODE, +) class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -121,14 +126,29 @@ class OpenThermGwOptionsFlow(config_entries.OptionsFlow): data_schema=vol.Schema( { vol.Optional( - CONF_PRECISION, - default=self.config_entry.options.get(CONF_PRECISION, 0), + CONF_READ_PRECISION, + default=self.config_entry.options.get(CONF_READ_PRECISION, 0), ): vol.All( vol.Coerce(float), vol.In( [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), ), + vol.Optional( + CONF_SET_PRECISION, + default=self.config_entry.options.get(CONF_SET_PRECISION, 0), + ): vol.All( + vol.Coerce(float), + vol.In( + [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + ), + ), + vol.Optional( + CONF_TEMPORARY_OVRD_MODE, + default=self.config_entry.options.get( + CONF_TEMPORARY_OVRD_MODE, True + ), + ): bool, vol.Optional( CONF_FLOOR_TEMP, default=self.config_entry.options.get(CONF_FLOOR_TEMP, False), diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 2c3e2f7071d..09713a69e54 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -19,6 +19,9 @@ ATTR_CH_OVRD = "ch_override" CONF_CLIMATE = "climate" CONF_FLOOR_TEMP = "floor_temperature" CONF_PRECISION = "precision" +CONF_READ_PRECISION = "read_precision" +CONF_SET_PRECISION = "set_precision" +CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 4a20aa651cd..1d9904ea59f 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -2,11 +2,11 @@ import logging from pprint import pformat -from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN @@ -77,7 +77,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class OpenThermSensor(Entity): +class OpenThermSensor(SensorEntity): """Representation of an OpenTherm Gateway sensor.""" def __init__(self, gw_dev, var, source, device_class, unit, friendly_name_format): diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index 306529e7be1..ed9cf05cae8 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -22,7 +22,9 @@ "description": "Options for the OpenTherm Gateway", "data": { "floor_temperature": "Floor Temperature", - "precision": "Precision" + "read_precision": "Read Precision", + "set_precision": "Set Precision", + "temporary_override_mode": "Temporary Setpoint Override Mode" } } } diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 1da9bbb584e..3f38055fb8c 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "Temperatura de la planta", - "precision": "Precisi\u00f3" + "precision": "Precisi\u00f3", + "read_precision": "Llegeix precisi\u00f3", + "set_precision": "Defineix precisi\u00f3", + "temporary_override_mode": "Mode de sobreescriptura temporal" }, "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d'OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/el.json b/homeassistant/components/opentherm_gw/translations/el.json new file mode 100644 index 00000000000..f15bc7bdc0e --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/el.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "read_precision": "\u0394\u03b9\u03ac\u03b2\u03b1\u03c3\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "set_precision": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json index 9d74a168bae..a4e9eb664b2 100644 --- a/homeassistant/components/opentherm_gw/translations/en.json +++ b/homeassistant/components/opentherm_gw/translations/en.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "Floor Temperature", - "precision": "Precision" + "precision": "Precision", + "read_precision": "Read Precision", + "set_precision": "Set Precision", + "temporary_override_mode": "Temporary Setpoint Override Mode" }, "description": "Options for the OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json index 935be180777..3b3a32987be 100644 --- a/homeassistant/components/opentherm_gw/translations/es-419.json +++ b/homeassistant/components/opentherm_gw/translations/es-419.json @@ -20,7 +20,9 @@ "init": { "data": { "floor_temperature": "Temperatura del piso", - "precision": "Precisi\u00f3n" + "precision": "Precisi\u00f3n", + "read_precision": "Leer precisi\u00f3n", + "set_precision": "Establecer precisi\u00f3n" }, "description": "Opciones para OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 44b6c6dfabc..7a85b685e89 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Temperatura del suelo", - "precision": "Precisi\u00f3n" + "precision": "Precisi\u00f3n", + "read_precision": "Leer precisi\u00f3n", + "set_precision": "Establecer precisi\u00f3n" }, "description": "Opciones para OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json index 4ab500e5531..9aef362a6a0 100644 --- a/homeassistant/components/opentherm_gw/translations/et.json +++ b/homeassistant/components/opentherm_gw/translations/et.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "P\u00f5randa temperatuur", - "precision": "T\u00e4psus" + "precision": "T\u00e4psus", + "read_precision": "Lugemi t\u00e4psus", + "set_precision": "M\u00e4\u00e4ra lugemi t\u00e4psus", + "temporary_override_mode": "Ajutine seadepunkti alistamine" }, "description": "OpenTherm Gateway suvandid" } diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index f060503ea23..7cc5b4ef848 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Temp\u00e9rature du sol", - "precision": "Pr\u00e9cision" + "precision": "Pr\u00e9cision", + "read_precision": "Pr\u00e9cision de lecture", + "set_precision": "D\u00e9finir la pr\u00e9cision" }, "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 c3dd3f10206..b8f51f4bb20 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "error": { - "already_configured": "Az \u00e1tj\u00e1r\u00f3 m\u00e1r konfigur\u00e1lva van", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik" }, "step": { diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json new file mode 100644 index 00000000000..7c7624c3dfe --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "id_exists": "ID gateway sudah ada" + }, + "step": { + "init": { + "data": { + "device": "Jalur atau URL", + "id": "ID", + "name": "Nama" + }, + "title": "Gateway OpenTherm" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Suhu Lantai", + "precision": "Tingkat Presisi" + }, + "description": "Pilihan untuk Gateway OpenTherm" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json index df1c36cd8d5..a082cd87586 100644 --- a/homeassistant/components/opentherm_gw/translations/it.json +++ b/homeassistant/components/opentherm_gw/translations/it.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "Temperatura del pavimento", - "precision": "Precisione" + "precision": "Precisione", + "read_precision": "Leggi la precisione", + "set_precision": "Imposta la precisione", + "temporary_override_mode": "Modalit\u00e0 di esclusione temporanea del setpoint" }, "description": "Opzioni per OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index 6f3ac939ad1..00f2902a4f3 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", - "precision": "\uc815\ubc00\ub3c4" + "precision": "\uc815\ubc00\ub3c4", + "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4", + "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30" }, "description": "OpenTherm Gateway \uc635\uc158" } diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index e832e790c1e..bdd3337d05b 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Vloertemperatuur", - "precision": "Precisie" + "precision": "Precisie", + "read_precision": "Lees Precisie", + "set_precision": "Precisie instellen" }, "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 76118924e0a..07b7c77c5cc 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Etasje Temperatur", - "precision": "Presisjon" + "precision": "Presisjon", + "read_precision": "Les presisjon", + "set_precision": "Angi presisjon" }, "description": "Alternativer for OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index 3fe12393a14..dc06752e404 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", - "precision": "Precyzja" + "precision": "Precyzja", + "read_precision": "Odczytaj precyzj\u0119", + "set_precision": "Ustaw precyzj\u0119" }, "description": "Opcje dla bramki OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json index e63bfb58d95..3b10be1166a 100644 --- a/homeassistant/components/opentherm_gw/translations/ru.json +++ b/homeassistant/components/opentherm_gw/translations/ru.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", - "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c" + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c", + "read_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u044f", + "set_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", + "temporary_override_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u0432\u043a\u0438" }, "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm" } diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index ea138287c78..8273eb1de98 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -21,7 +21,10 @@ "init": { "data": { "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", - "precision": "\u6e96\u78ba\u5ea6" + "precision": "\u6e96\u78ba\u5ea6", + "read_precision": "\u8b80\u53d6\u7cbe\u6e96\u5ea6", + "set_precision": "\u8a2d\u5b9a\u7cbe\u6e96\u5ea6", + "temporary_override_mode": "\u81e8\u6642 Setpoint \u8986\u84cb\u6a21\u5f0f" }, "description": "OpenTherm \u9598\u9053\u5668\u9078\u9805" } diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index ce75365771d..aeefe435845 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -69,9 +69,9 @@ async def async_setup_entry(hass, config_entry): LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) @_verify_domain_control @@ -110,8 +110,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -187,7 +187,7 @@ class OpenUvEntity(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 9dfb053ff01..172b0ed3c44 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index b9c73023c11..654a89cfcf9 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -1,4 +1,5 @@ """Support for OpenUV sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import TIME_MINUTES, UV_INDEX from homeassistant.core import callback from homeassistant.util.dt import as_local, parse_datetime @@ -87,7 +88,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors, True) -class OpenUvSensor(OpenUvEntity): +class OpenUvSensor(OpenUvEntity, SensorEntity): """Define a binary sensor for OpenUV.""" def __init__(self, openuv, sensor_type, name, icon, unit, entry_id): diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index 3a6f6a8ae91..b5c0e5ec608 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, "error": { "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" }, diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 4d0edd93ea9..5075ec2c965 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -1,15 +1,18 @@ { "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, "error": { "invalid_api_key": "Kunci API tidak valid" }, "step": { "user": { "data": { - "api_key": "Kunci API OpenUV", + "api_key": "Kunci API", "elevation": "Ketinggian", "latitude": "Lintang", - "longitude": "Garis bujur" + "longitude": "Bujur" }, "title": "Isi informasi Anda" } diff --git a/homeassistant/components/openuv/translations/ko.json b/homeassistant/components/openuv/translations/ko.json index ee211d3cbd5..114be1df692 100644 --- a/homeassistant/components/openuv/translations/ko.json +++ b/homeassistant/components/openuv/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index 118e8f05141..d7287b99ddf 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd." + "already_configured": "Locatie is al geconfigureerd." }, "error": { "invalid_api_key": "Ongeldige API-sleutel" @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "OpenUV API-Sleutel", + "api_key": "API-sleutel", "elevation": "Hoogte", "latitude": "Breedtegraad", "longitude": "Lengtegraad" diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 4754c4b2eff..f6d47d1dcae 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -14,10 +14,8 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .const import ( - COMPONENTS, CONF_LANGUAGE, CONFIG_FLOW_VERSION, DOMAIN, @@ -25,6 +23,7 @@ from .const import ( ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_FREE_DAILY, FORECAST_MODE_ONECALL_DAILY, + PLATFORMS, UPDATE_LISTENER, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -54,10 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): owm, latitude, longitude, forecast_mode, hass ) - await weather_coordinator.async_refresh() - - if not weather_coordinator.last_update_success: - raise ConfigEntryNotReady + await weather_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = { @@ -65,9 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for component in COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) update_listener = config_entry.add_update_listener(async_update_options) @@ -108,8 +104,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in COMPONENTS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py index 809d2c2e572..30a21a057f0 100644 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -1,12 +1,12 @@ """Abstraction form OWM sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT -class AbstractOpenWeatherMapSensor(Entity): +class AbstractOpenWeatherMapSensor(SensorEntity): """Abstract class for an OpenWeatherMap sensor.""" def __init__( @@ -57,7 +57,7 @@ class AbstractOpenWeatherMapSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 2c2070141d5..7be4fe795ac 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -20,10 +20,10 @@ from .const import ( DEFAULT_FORECAST_MODE, DEFAULT_LANGUAGE, DEFAULT_NAME, + DOMAIN, FORECAST_MODES, LANGUAGES, ) -from .const import DOMAIN # pylint:disable=unused-import class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index c70afa9cab0..36d38ff4688 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -16,6 +16,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, @@ -34,6 +35,7 @@ from homeassistant.const import ( PRESSURE_HPA, SPEED_METERS_PER_SECOND, TEMP_CELSIUS, + UV_INDEX, ) DOMAIN = "openweathermap" @@ -45,9 +47,12 @@ CONFIG_FLOW_VERSION = 2 ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ATTR_API_PRECIPITATION = "precipitation" +ATTR_API_PRECIPITATION_KIND = "precipitation_kind" ATTR_API_DATETIME = "datetime" +ATTR_API_DEW_POINT = "dew_point" ATTR_API_WEATHER = "weather" ATTR_API_TEMPERATURE = "temperature" +ATTR_API_FEELS_LIKE_TEMPERATURE = "feels_like_temperature" ATTR_API_WIND_SPEED = "wind_speed" ATTR_API_WIND_BEARING = "wind_bearing" ATTR_API_HUMIDITY = "humidity" @@ -56,13 +61,14 @@ ATTR_API_CONDITION = "condition" ATTR_API_CLOUDS = "clouds" ATTR_API_RAIN = "rain" ATTR_API_SNOW = "snow" +ATTR_API_UV_INDEX = "uv_index" ATTR_API_WEATHER_CODE = "weather_code" ATTR_API_FORECAST = "forecast" SENSOR_NAME = "sensor_name" SENSOR_UNIT = "sensor_unit" SENSOR_DEVICE_CLASS = "sensor_device_class" UPDATE_LISTENER = "update_listener" -COMPONENTS = ["sensor", "weather"] +PLATFORMS = ["sensor", "weather"] FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" @@ -79,7 +85,9 @@ DEFAULT_FORECAST_MODE = FORECAST_MODE_ONECALL_DAILY MONITORED_CONDITIONS = [ ATTR_API_WEATHER, + ATTR_API_DEW_POINT, ATTR_API_TEMPERATURE, + ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_WIND_SPEED, ATTR_API_WIND_BEARING, ATTR_API_HUMIDITY, @@ -87,12 +95,15 @@ MONITORED_CONDITIONS = [ ATTR_API_CLOUDS, ATTR_API_RAIN, ATTR_API_SNOW, + ATTR_API_PRECIPITATION_KIND, + ATTR_API_UV_INDEX, ATTR_API_CONDITION, ATTR_API_WEATHER_CODE, ] FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, @@ -183,11 +194,21 @@ CONDITION_CLASSES = { } WEATHER_SENSOR_TYPES = { ATTR_API_WEATHER: {SENSOR_NAME: "Weather"}, + ATTR_API_DEW_POINT: { + SENSOR_NAME: "Dew Point", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, ATTR_API_TEMPERATURE: { SENSOR_NAME: "Temperature", SENSOR_UNIT: TEMP_CELSIUS, SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, }, + ATTR_API_FEELS_LIKE_TEMPERATURE: { + SENSOR_NAME: "Feels like temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, ATTR_API_WIND_SPEED: { SENSOR_NAME: "Wind speed", SENSOR_UNIT: SPEED_METERS_PER_SECOND, @@ -206,12 +227,24 @@ WEATHER_SENSOR_TYPES = { ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE}, ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: LENGTH_MILLIMETERS}, ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: LENGTH_MILLIMETERS}, + ATTR_API_PRECIPITATION_KIND: {SENSOR_NAME: "Precipitation kind"}, + ATTR_API_UV_INDEX: { + SENSOR_NAME: "UV Index", + SENSOR_UNIT: UV_INDEX, + }, ATTR_API_CONDITION: {SENSOR_NAME: "Condition"}, ATTR_API_WEATHER_CODE: {SENSOR_NAME: "Weather Code"}, } FORECAST_SENSOR_TYPES = { ATTR_FORECAST_CONDITION: {SENSOR_NAME: "Condition"}, - ATTR_FORECAST_PRECIPITATION: {SENSOR_NAME: "Precipitation"}, + ATTR_FORECAST_PRECIPITATION: { + SENSOR_NAME: "Precipitation", + SENSOR_UNIT: LENGTH_MILLIMETERS, + }, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: { + SENSOR_NAME: "Precipitation probability", + SENSOR_UNIT: PERCENTAGE, + }, ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"}, ATTR_FORECAST_TEMP: { SENSOR_NAME: "Temperature", diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index e355e2e4752..27cda9fb26d 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -3,6 +3,6 @@ "name": "OpenWeatherMap", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", - "requirements": ["pyowm==3.1.1"], + "requirements": ["pyowm==3.2.0"], "codeowners": ["@fabaff", "@freekode", "@nzapponi"] } diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/hu.json b/homeassistant/components/openweathermap/translations/hu.json new file mode 100644 index 00000000000..2fd2f0acc7a --- /dev/null +++ b/homeassistant/components/openweathermap/translations/hu.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "language": "Nyelv", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "mode": "M\u00f3d", + "name": "Az integr\u00e1ci\u00f3 neve" + }, + "description": "Az OpenWeatherMap integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API kulcs l\u00e9trehoz\u00e1s\u00e1hoz menj az https://openweathermap.org/appid oldalra", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Nyelv", + "mode": "M\u00f3d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/id.json b/homeassistant/components/openweathermap/translations/id.json new file mode 100644 index 00000000000..61d2713f42a --- /dev/null +++ b/homeassistant/components/openweathermap/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "language": "Bahasa", + "latitude": "Lintang", + "longitude": "Bujur", + "mode": "Mode", + "name": "Nama integrasi" + }, + "description": "Siapkan integrasi OpenWeatherMap. Untuk membuat kunci API, buka https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Bahasa", + "mode": "Mode" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json index 1514ada24ec..d2f5d9aa123 100644 --- a/homeassistant/components/openweathermap/translations/ko.json +++ b/homeassistant/components/openweathermap/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "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", @@ -17,7 +17,7 @@ "mode": "\ubaa8\ub4dc", "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" }, - "description": "OpenWeatherMap \ud1b5\ud569\uc744 \uc124\uc815\ud558\uc138\uc694. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid\ub85c \uc774\ub3d9\ud558\uc2ed\uc2dc\uc624.", + "description": "OpenWeatherMap \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index e07a8f32608..51e475eb754 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, @@ -24,12 +25,16 @@ from homeassistant.util import dt from .const import ( ATTR_API_CLOUDS, ATTR_API_CONDITION, + ATTR_API_DEW_POINT, + ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_FORECAST, ATTR_API_HUMIDITY, + ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, ATTR_API_RAIN, ATTR_API_SNOW, ATTR_API_TEMPERATURE, + ATTR_API_UV_INDEX, ATTR_API_WEATHER, ATTR_API_WEATHER_CODE, ATTR_API_WIND_BEARING, @@ -114,6 +119,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_API_TEMPERATURE: current_weather.temperature("celsius").get("temp"), + ATTR_API_FEELS_LIKE_TEMPERATURE: current_weather.temperature("celsius").get( + "feels_like" + ), + ATTR_API_DEW_POINT: (round(current_weather.dewpoint / 100, 1)), ATTR_API_PRESSURE: current_weather.pressure.get("press"), ATTR_API_HUMIDITY: current_weather.humidity, ATTR_API_WIND_BEARING: current_weather.wind().get("deg"), @@ -121,8 +130,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_API_CLOUDS: current_weather.clouds, ATTR_API_RAIN: self._get_rain(current_weather.rain), ATTR_API_SNOW: self._get_snow(current_weather.snow), + ATTR_API_PRECIPITATION_KIND: self._calc_precipitation_kind( + current_weather.rain, current_weather.snow + ), ATTR_API_WEATHER: current_weather.detailed_status, ATTR_API_CONDITION: self._get_condition(current_weather.weather_code), + ATTR_API_UV_INDEX: current_weather.uvi, ATTR_API_WEATHER_CODE: current_weather.weather_code, ATTR_API_FORECAST: forecast_weather, } @@ -145,6 +158,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( + round(entry.precipitation_probability * 100) + ), ATTR_FORECAST_PRESSURE: entry.pressure.get("press"), ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"), ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), @@ -166,36 +182,45 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_rain(rain): """Get rain data from weather data.""" if "all" in rain: - return round(rain["all"], 0) + return round(rain["all"], 2) if "1h" in rain: - return round(rain["1h"], 0) - return "not raining" + return round(rain["1h"], 2) + return 0 @staticmethod def _get_snow(snow): """Get snow data from weather data.""" if snow: if "all" in snow: - return round(snow["all"], 0) + return round(snow["all"], 2) if "1h" in snow: - return round(snow["1h"], 0) - return "not snowing" - return "not snowing" + return round(snow["1h"], 2) + return 0 @staticmethod def _calc_precipitation(rain, snow): """Calculate the precipitation.""" rain_value = 0 - if WeatherUpdateCoordinator._get_rain(rain) != "not raining": + if WeatherUpdateCoordinator._get_rain(rain) != 0: rain_value = WeatherUpdateCoordinator._get_rain(rain) snow_value = 0 - if WeatherUpdateCoordinator._get_snow(snow) != "not snowing": + if WeatherUpdateCoordinator._get_snow(snow) != 0: snow_value = WeatherUpdateCoordinator._get_snow(snow) - if round(rain_value + snow_value, 1) == 0: - return None - return round(rain_value + snow_value, 1) + return round(rain_value + snow_value, 2) + + @staticmethod + def _calc_precipitation_kind(rain, snow): + """Determine the precipitation kind.""" + if WeatherUpdateCoordinator._get_rain(rain) != 0: + if WeatherUpdateCoordinator._get_snow(snow) != 0: + return "Snow and Rain" + return "Rain" + + if WeatherUpdateCoordinator._get_snow(snow) != 0: + return "Snow" + return "None" def _get_condition(self, weather_code, timestamp=None): """Get weather condition from weather data.""" diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index d6620ed39e5..063c0c6169d 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -5,10 +5,9 @@ import logging from oru import Meter, MeterError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ENERGY_KILO_WATT_HOUR import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -39,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Oru meter_number = %s", meter_number) -class CurrentEnergyUsageSensor(Entity): +class CurrentEnergyUsageSensor(SensorEntity): """Representation of the sensor.""" def __init__(self, meter): diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index a03d9fc5ff0..f7a16036f00 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): switch_conf = config.get(CONF_SWITCHES, [config]) if config.get(CONF_DISCOVERY): - _LOGGER.info("Discovering S20 switches ...") + _LOGGER.info("Discovering S20 switches") switch_data.update(discover()) for switch in switch_conf: diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 49c32da69bc..e01ad970488 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -269,7 +269,7 @@ class Luminary(LightEntity): return self._unique_id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return self._device_attributes diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 9f23f5ae6fa..7aee9d99208 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -4,11 +4,10 @@ import time import pyotp import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_TOKEN from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity DEFAULT_NAME = "OTP Sensor" @@ -34,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Only TOTP supported at the moment, HOTP might be added later -class TOTPSensor(Entity): +class TOTPSensor(SensorEntity): """Representation of a TOTP sensor.""" def __init__(self, name, token): diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 0130ba30c30..98ed42ea10e 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -1,14 +1,16 @@ """Support for OVO Energy.""" +from __future__ import annotations + from datetime import datetime, timedelta import logging -from typing import Any, Dict +from typing import Any import aiohttp import async_timeout from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -44,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if not authenticated: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data + DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) ) return False @@ -61,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if not authenticated: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data + DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) ) raise UpdateFailed("Not authenticated with OVO Energy") @@ -84,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool } # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() # Setup components hass.async_create_task( @@ -148,7 +150,7 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): """Defines a OVO Energy device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this OVO Energy instance.""" return { "identifiers": {(DOMAIN, self._client.account_id)}, diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 0b2f7aac2d0..f65b8007ecb 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) USER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 2f2e1b8dd50..d03f7c49f96 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta from ovoenergy import OVODailyUsage 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.helpers.update_coordinator import DataUpdateCoordinator @@ -53,7 +54,7 @@ async def async_setup_entry( async_add_entities(entities, True) -class OVOEnergySensor(OVOEnergyDeviceEntity): +class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity): """Defines a OVO Energy sensor.""" def __init__( @@ -100,7 +101,7 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): return usage.electricity[-1].consumption @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return the attributes of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: @@ -135,7 +136,7 @@ class OVOEnergyLastGasReading(OVOEnergySensor): return usage.gas[-1].consumption @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return the attributes of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: @@ -171,7 +172,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): return usage.electricity[-1].cost.amount @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return the attributes of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: @@ -207,7 +208,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): return usage.gas[-1].cost.amount @property - def device_state_attributes(self) -> object: + def extra_state_attributes(self) -> object: """Return the attributes of the sensor.""" usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 761f6a7d247..a86f39a614c 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -10,7 +10,9 @@ "reauth": { "data": { "password": "Passwort" - } + }, + "description": "Die Authentifizierung f\u00fcr OVO Energy ist fehlgeschlagen. Bitte geben Sie Ihre aktuellen Anmeldedaten ein.", + "title": "Erneute Authentifizierung" }, "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index c4091388b35..7bfd337cce5 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -2,11 +2,23 @@ "config": { "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { + "data": { + "password": "Jelsz\u00f3" + }, "title": "\u00dajrahiteles\u00edt\u00e9s" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "OVO Energy azonos\u00edt\u00f3 megad\u00e1sa" } } } diff --git a/homeassistant/components/ovo_energy/translations/id.json b/homeassistant/components/ovo_energy/translations/id.json new file mode 100644 index 00000000000..05c38f244e7 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "OVO Energy: {username}", + "step": { + "reauth": { + "data": { + "password": "Kata Sandi" + }, + "description": "Autentikasi gagal untuk OVO Energy. Masukkan kredensial Anda saat ini.", + "title": "Autentikasi ulang" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Siapkan instans OVO Energy untuk mengakses data penggunaan energi Anda.", + "title": "Tambahkan Akun OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ko.json b/homeassistant/components/ovo_energy/translations/ko.json index 07ef8d8e166..4ca904725b4 100644 --- a/homeassistant/components/ovo_energy/translations/ko.json +++ b/homeassistant/components/ovo_energy/translations/ko.json @@ -5,17 +5,21 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { "data": { "password": "\ube44\ubc00\ubc88\ud638" - } + }, + "description": "OVO Energy\uc5d0 \ub300\ud55c \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ud604\uc7ac \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\uc7ac\uc778\uc99d" }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, + "description": "\uc5d0\ub108\uc9c0 \uc0ac\uc6a9\ub7c9\uc5d0 \uc811\uadfc\ud558\ub824\uba74 OVO Energy \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "OVO Energy \uacc4\uc815 \ucd94\uac00\ud558\uae30" } } diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json index 7a2b5b757bb..d598e17d93b 100644 --- a/homeassistant/components/ovo_energy/translations/nl.json +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -18,7 +18,9 @@ "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "description": "Stel een OVO Energy instance in om toegang te krijgen tot je energieverbruik.", + "title": "Voeg OVO Energie Account toe" } } } diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index 47a94f6a24a..89eb632102f 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OVO Energy.", "title": "OVO Energy" diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index f0838b510ec..7074312594a 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -4,7 +4,7 @@ import secrets from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID -from .const import DOMAIN # noqa pylint: disable=unused-import +from .const import DOMAIN from .helper import supports_encryption CONF_SECRET = "secret" diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 8a8fdc52fb1..d50e5b9c414 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -74,7 +74,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): return self._data.get("battery") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific attributes.""" return self._data.get("attributes") diff --git a/homeassistant/components/owntracks/translations/hu.json b/homeassistant/components/owntracks/translations/hu.json index b6bb7593906..f103fc9bbe1 100644 --- a/homeassistant/components/owntracks/translations/hu.json +++ b/homeassistant/components/owntracks/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "create_entry": { "default": "\n\nAndroidon, nyisd meg [az OwnTracks appot]({android_url}), menj a preferences -> connectionre. V\u00e1ltoztasd meg a al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyisd meg [az OwnTracks appot]({ios_url}), kattints az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztasd meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zd meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." }, diff --git a/homeassistant/components/owntracks/translations/id.json b/homeassistant/components/owntracks/translations/id.json new file mode 100644 index 00000000000..890afaa099c --- /dev/null +++ b/homeassistant/components/owntracks/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "create_entry": { + "default": "\n\nDi Android, buka [aplikasi OwnTracks]({android_url}), buka preferensi -> koneksi. Ubah setelan berikut ini:\n - Mode: HTTP Pribadi\n - Host: {webhook_url}\n - Identifikasi:\n - Nama pengguna: `''`\n - ID Perangkat: `''`\n\nDi iOS, buka [aplikasi OwnTracks]({ios_url}), ketuk ikon (i) di pojok kiri atas -> pengaturan. Ubah setelan berikut ini:\n - Mode: HTTP\n - URL: {webhook_url}\n - Aktifkan autentikasi\n - UserID: `''`\n\n{secret}\n\nLihat [dokumentasi]({docs_url}) untuk informasi lebih lanjut." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan OwnTracks?", + "title": "Siapkan OwnTracks" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/ko.json b/homeassistant/components/owntracks/translations/ko.json index 6e558e54627..a8dd94b7e16 100644 --- a/homeassistant/components/owntracks/translations/ko.json +++ b/homeassistant/components/owntracks/translations/ko.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "create_entry": { - "default": "\n\nAndroid \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url}) \uc744 \uc5f4\uace0 preferences -> connection \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url}) \uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "\n\nAndroid\uc778 \uacbd\uc6b0, [OwnTracks \uc571]({android_url})\uc744 \uc5f4\uace0 preferences -> connection\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\niOS \uc778 \uacbd\uc6b0, [OwnTracks \uc571]({ios_url})\uc744 \uc5f4\uace0 \uc67c\ucabd \uc0c1\ub2e8\uc758 (i) \uc544\uc774\ucf58\uc744 \ud0ed\ud558\uc5ec \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \ub2e4\uc74c\uacfc \uac19\uc774 \uc124\uc815\ud574\uc8fc\uc138\uc694:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret} \n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "OwnTracks \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "OwnTracks\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "OwnTracks \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index a75c05416dc..ace71e4af81 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -1,5 +1,6 @@ """The ozw integration.""" import asyncio +from contextlib import suppress import json import logging @@ -267,8 +268,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def start_platforms(): await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS ] ) if entry.data.get(CONF_USE_ADDON): @@ -280,10 +281,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): Do not unsubscribe the manager topic. """ mqtt_client_task.cancel() - try: + with suppress(asyncio.CancelledError): await mqtt_client_task - except asyncio.CancelledError: - pass ozw_data[DATA_UNSUBSCRIBE].append( hass.bus.async_listen_once( @@ -310,8 +309,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index a74fd869f0f..e403c4f5517 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -1,7 +1,8 @@ """Support for Z-Wave climate devices.""" +from __future__ import annotations + from enum import IntEnum import logging -from typing import Optional, Tuple from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( @@ -239,12 +240,12 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): return self._current_mode_setpoint_values[0].value @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._current_mode_setpoint_values[0].value @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" return self._current_mode_setpoint_values[1].value @@ -308,9 +309,9 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): self.values.mode.send_value(preset_mode_value) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" - data = super().device_state_attributes + data = super().extra_state_attributes if self.values.fan_action: data[ATTR_FAN_ACTION] = self.values.fan_action.value if self.values.valve_position: @@ -333,7 +334,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): support |= SUPPORT_PRESET_MODE return support - def _get_current_mode_setpoint_values(self) -> Tuple: + def _get_current_mode_setpoint_values(self) -> tuple: """Return a tuple of current setpoint Z-Wave value(s).""" if not self.values.mode: setpoint_names = ("setpoint_heating",) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 00917c0609c..2546a2e0aff 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -7,8 +7,7 @@ from homeassistant import config_entries from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow -from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index c1cb9617a5c..305601a2333 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -209,7 +209,7 @@ class ZWaveDeviceEntity(Entity): return device_info @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" return {const.ATTR_NODE_ID: self.values.primary.node.node_id} diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py index 5bd0d1c482f..3c3d4c3ca36 100644 --- a/homeassistant/components/ozw/sensor.py +++ b/homeassistant/components/ozw/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DOMAIN as SENSOR_DOMAIN, + SensorEntity, ) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback @@ -57,7 +58,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class ZwaveSensorBase(ZWaveDeviceEntity): +class ZwaveSensorBase(ZWaveDeviceEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" @property @@ -149,9 +150,9 @@ class ZWaveListSensor(ZwaveSensorBase): return self.values.primary.value["Selected_id"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" - attributes = super().device_state_attributes + attributes = super().extra_state_attributes # add the value's label as property attributes["label"] = self.values.primary.value["Selected"] return attributes diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json index afa26fb7e03..c58c55c49ad 100644 --- a/homeassistant/components/ozw/translations/de.json +++ b/homeassistant/components/ozw/translations/de.json @@ -1,11 +1,17 @@ { "config": { "abort": { + "addon_info_failed": "Fehler beim Abrufen von OpenZWave Add-on Informationen.", + "addon_install_failed": "Installation des OpenZWave Add-ons fehlgeschlagen.", + "addon_set_config_failed": "Setzen der OpenZWave Konfiguration fehlgeschlagen.", "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "addon_start_failed": "Fehler beim Starten des OpenZWave Add-ons. \u00dcberpr\u00fcfe die Konfiguration." + }, "progress": { "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." }, @@ -17,13 +23,18 @@ "title": "Die Installation des OpenZWave-Add-On wurde gestartet" }, "on_supervisor": { + "data": { + "use_addon": "Verwende das OpenZWave Supervisor Add-on" + }, + "description": "M\u00f6chtest du das OpenZWave Supervisor Add-on verwenden?", "title": "Verbindungstyp ausw\u00e4hlen" }, "start_addon": { "data": { "network_key": "Netzwerk-Schl\u00fcssel", "usb_path": "USB-Ger\u00e4te-Pfad" - } + }, + "title": "Gib die Konfiguration des OpenZWave Add-ons ein" } } } diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json index bf4ba5c6995..5e408b7b807 100644 --- a/homeassistant/components/ozw/translations/fr.json +++ b/homeassistant/components/ozw/translations/fr.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "addon_info_failed": "Impossible d\u2019obtenir des informations de l'add-on OpenZWave.", - "addon_install_failed": "\u00c9chec de l\u2019installation de l'add-on OpenZWave.", + "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire OpenZWave.", + "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire OpenZWave.", "addon_set_config_failed": "\u00c9chec de la configuration OpenZWave.", "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", @@ -10,23 +10,23 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "addon_start_failed": "\u00c9chec du d\u00e9marrage de l'add-on OpenZWave. V\u00e9rifiez la configuration." + "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire OpenZWave. V\u00e9rifiez la configuration." }, "progress": { "install_addon": "Veuillez patienter pendant que l'installation du module OpenZWave se termine. Cela peut prendre plusieurs minutes." }, "step": { "hassio_confirm": { - "title": "Configurer l\u2019int\u00e9gration OpenZWave avec l\u2019add-on OpenZWave" + "title": "Configurer l'int\u00e9gration d'OpenZWave avec le module compl\u00e9mentaire OpenZWave" }, "install_addon": { "title": "L'installation du module compl\u00e9mentaire OpenZWave a commenc\u00e9" }, "on_supervisor": { "data": { - "use_addon": "Utiliser l'add-on OpenZWave Supervisor" + "use_addon": "Utilisez le module compl\u00e9mentaire OpenZWave du Supervisor" }, - "description": "Souhaitez-vous utiliser l'add-on OpenZWave Supervisor ?", + "description": "Voulez-vous utiliser le module compl\u00e9mentaire OpenZWave du Supervisor?", "title": "S\u00e9lectionner la m\u00e9thode de connexion" }, "start_addon": { @@ -34,7 +34,7 @@ "network_key": "Cl\u00e9 r\u00e9seau", "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, - "title": "Entrez dans la configuration de l'add-on OpenZWave" + "title": "Entrez dans la configuration du module compl\u00e9mentaire OpenZWave" } } } diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index e4c864d9fd3..6c2c6c22f55 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -4,7 +4,9 @@ "addon_info_failed": "Nem siker\u00fclt bet\u00f6lteni az OpenZWave kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3kat.", "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." @@ -12,14 +14,15 @@ "step": { "on_supervisor": { "data": { - "use_addon": "Haszn\u00e1lja az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt" + "use_addon": "Haszn\u00e1ld az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt" }, - "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt?", - "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" + "description": "Szeretn\u00e9d haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?", + "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" }, "start_addon": { "data": { - "network_key": "H\u00e1l\u00f3zati kulcs" + "network_key": "H\u00e1l\u00f3zati kulcs", + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" } } } diff --git a/homeassistant/components/ozw/translations/id.json b/homeassistant/components/ozw/translations/id.json new file mode 100644 index 00000000000..ef47e12f12d --- /dev/null +++ b/homeassistant/components/ozw/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "addon_info_failed": "Gagal mendapatkan info add-on OpenZWave.", + "addon_install_failed": "Gagal menginstal add-on OpenZWave.", + "addon_set_config_failed": "Gagal menyetel konfigurasi OpenZWave.", + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "mqtt_required": "Integrasi MQTT belum disiapkan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "addon_start_failed": "Gagal memulai add-on OpenZWave. Periksa konfigurasi." + }, + "progress": { + "install_addon": "Harap tunggu hingga penginstalan add-on OpenZWave selesai. Ini bisa memakan waktu beberapa saat." + }, + "step": { + "hassio_confirm": { + "title": "Siapkan integrasi OpenZWave dengan add-on OpenZWave" + }, + "install_addon": { + "title": "Instalasi add-on OpenZWave telah dimulai" + }, + "on_supervisor": { + "data": { + "use_addon": "Gunakan add-on Supervisor OpenZWave" + }, + "description": "Ingin menggunakan add-on Supervisor OpenZWave?", + "title": "Pilih metode koneksi" + }, + "start_addon": { + "data": { + "network_key": "Kunci Jaringan", + "usb_path": "Jalur Perangkat USB" + }, + "title": "Masukkan konfigurasi add-on OpenZWave" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ko.json b/homeassistant/components/ozw/translations/ko.json index ba37dccdd68..f6dddf5c96a 100644 --- a/homeassistant/components/ozw/translations/ko.json +++ b/homeassistant/components/ozw/translations/ko.json @@ -1,16 +1,40 @@ { "config": { "abort": { + "addon_info_failed": "OpenZWave \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "addon_install_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "addon_set_config_failed": "OpenZWave \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." + }, + "error": { + "addon_start_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "progress": { + "install_addon": "Openzwave \uc560\ub4dc\uc628\uc758 \uc124\uce58\uac00 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ubd84 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { + "hassio_confirm": { + "title": "OpenZWave \uc560\ub4dc\uc628\uc73c\ub85c OpenZWave \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30" + }, + "install_addon": { + "title": "Openzwave \uc560\ub4dc\uc628 \uc124\uce58\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "on_supervisor": { + "data": { + "use_addon": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uae30" + }, + "description": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc5f0\uacb0 \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, "start_addon": { "data": { + "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - } + }, + "title": "OpenZWave \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" } } } diff --git a/homeassistant/components/ozw/translations/nl.json b/homeassistant/components/ozw/translations/nl.json index 80ef72a061e..7392f9b63eb 100644 --- a/homeassistant/components/ozw/translations/nl.json +++ b/homeassistant/components/ozw/translations/nl.json @@ -1,11 +1,20 @@ { "config": { "abort": { + "addon_info_failed": "Mislukt om OpenZWave add-on info te krijgen.", + "addon_install_failed": "De installatie van de OpenZWave add-on is mislukt.", + "addon_set_config_failed": "Mislukt om OpenZWave configuratie in te stellen.", "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", - "mqtt_required": "De [%%] integratie is niet ingesteld", + "mqtt_required": "De MQTT-integratie is niet ingesteld", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, + "error": { + "addon_start_failed": "Het starten van de OpenZWave-add-on is mislukt. Controleer de configuratie." + }, + "progress": { + "install_addon": "Wacht even terwijl de installatie van de OpenZWave add-on wordt voltooid. Dit kan enkele minuten duren." + }, "step": { "hassio_confirm": { "title": "OpenZWave integratie instellen met de OpenZWave add-on" @@ -13,10 +22,19 @@ "install_addon": { "title": "De OpenZWave add-on installatie is gestart" }, + "on_supervisor": { + "data": { + "use_addon": "Gebruik de OpenZWave Supervisor add-on" + }, + "description": "Wilt u de OpenZWave Supervisor add-on gebruiken?", + "title": "Selecteer een verbindingsmethode" + }, "start_addon": { "data": { + "network_key": "Netwerksleutel", "usb_path": "USB-apparaatpad" - } + }, + "title": "Voer de OpenZWave add-on configuratie in" } } } diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index f9ed5469da0..37ab2ea9c9e 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -1,32 +1,32 @@ { "config": { "abort": { - "addon_info_failed": "\u53d6\u5f97 OpenZWave add-on \u8cc7\u8a0a\u5931\u6557\u3002", - "addon_install_failed": "OpenZWave add-on \u5b89\u88dd\u5931\u6557\u3002", - "addon_set_config_failed": "OpenZWave add-on \u8a2d\u5b9a\u5931\u6557\u3002", + "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_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" }, "error": { - "addon_start_failed": "OpenZWave add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" + "addon_start_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" }, "progress": { - "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" }, "step": { "hassio_confirm": { - "title": "\u4ee5 OpenZWave add-on \u8a2d\u5b9a OpenZwave \u6574\u5408" + "title": "\u4ee5 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a OpenZwave \u6574\u5408" }, "install_addon": { - "title": "OpenZWave add-on \u5b89\u88dd\u5df2\u555f\u52d5" + "title": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5" }, "on_supervisor": { "data": { - "use_addon": "\u4f7f\u7528 OpenZWave Supervisor add-on" + "use_addon": "\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6" }, - "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor add-on\uff1f", + "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" }, "start_addon": { @@ -34,7 +34,7 @@ "network_key": "\u7db2\u8def\u5bc6\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "title": "\u8acb\u8f38\u5165 OpenZWave \u8a2d\u5b9a\u3002" + "title": "\u8acb\u8f38\u5165 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u3002" } } } diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 3305c935890..67cf07dc433 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -8,6 +8,7 @@ from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv @@ -46,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = [MEDIA_PLAYER_DOMAIN] +PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] async def async_setup(hass, config): @@ -93,7 +94,7 @@ async def async_setup_entry(hass, config_entry): unique_id = config_entry.unique_id if device_info is None: _LOGGER.error( - "Couldn't gather device info. Please restart Home Assistant with your TV turned on and connected to your network." + "Couldn't gather device info; Please restart Home Assistant with your TV turned on and connected to your network" ) else: unique_id = device_info[ATTR_UDN] @@ -103,9 +104,9 @@ async def async_setup_entry(hass, config_entry): data={**config, ATTR_DEVICE_INFO: device_info}, ) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -116,8 +117,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -219,6 +220,7 @@ class Remote: """Turn off the TV.""" if self.state != STATE_OFF: await self.async_send_key(Keys.power) + self.state = STATE_OFF await self.async_update() async def async_set_mute(self, enable): diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index b39d3a1d3c8..50a030b91da 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT -from .const import ( # pylint: disable=unused-import +from .const import ( ATTR_DEVICE_INFO, ATTR_FRIENDLY_NAME, ATTR_UDN, diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py new file mode 100644 index 00000000000..8f3fab80215 --- /dev/null +++ b/homeassistant/components/panasonic_viera/remote.py @@ -0,0 +1,90 @@ +"""Remote control support for Panasonic Viera TV.""" +import logging + +from homeassistant.components.remote import RemoteEntity +from homeassistant.const import CONF_NAME, STATE_ON + +from .const import ( + ATTR_DEVICE_INFO, + ATTR_MANUFACTURER, + ATTR_MODEL_NUMBER, + ATTR_REMOTE, + ATTR_UDN, + DEFAULT_MANUFACTURER, + DEFAULT_MODEL_NUMBER, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Panasonic Viera TV Remote from a config entry.""" + + config = config_entry.data + + remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE] + name = config[CONF_NAME] + device_info = config[ATTR_DEVICE_INFO] + + async_add_entities([PanasonicVieraRemoteEntity(remote, name, device_info)]) + + +class PanasonicVieraRemoteEntity(RemoteEntity): + """Representation of a Panasonic Viera TV Remote.""" + + def __init__(self, remote, name, device_info): + """Initialize the entity.""" + # Save a reference to the imported class + self._remote = remote + self._name = name + self._device_info = device_info + + @property + def unique_id(self): + """Return the unique ID of the device.""" + if self._device_info is None: + return None + return self._device_info[ATTR_UDN] + + @property + def device_info(self): + """Return device specific attributes.""" + if self._device_info is None: + return None + return { + "name": self._name, + "identifiers": {(DOMAIN, self._device_info[ATTR_UDN])}, + "manufacturer": self._device_info.get( + ATTR_MANUFACTURER, DEFAULT_MANUFACTURER + ), + "model": self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER), + } + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def available(self): + """Return True if the device is available.""" + return self._remote.available + + @property + def is_on(self): + """Return true if device is on.""" + return self._remote.state == STATE_ON + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._remote.async_turn_on(context=self._context) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._remote.async_turn_off() + + async def async_send_command(self, command, **kwargs): + """Send a command to one device.""" + for cmd in command: + await self._remote.async_send_key(cmd) diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index fbf7f49be6a..cfc0be387d0 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -1,16 +1,28 @@ { "config": { "abort": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_pin_code": "A megadott PIN-k\u00f3d \u00e9rv\u00e9nytelen volt" }, "step": { + "pairing": { + "data": { + "pin": "PIN-k\u00f3d" + }, + "description": "Add meg a TV-k\u00e9sz\u00fcl\u00e9ken megjelen\u0151 PIN-k\u00f3dot", + "title": "P\u00e1ros\u00edt\u00e1s" + }, "user": { "data": { - "host": "IP c\u00edm" - } + "host": "IP c\u00edm", + "name": "N\u00e9v" + }, + "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet" } } } diff --git a/homeassistant/components/panasonic_viera/translations/id.json b/homeassistant/components/panasonic_viera/translations/id.json new file mode 100644 index 00000000000..4f9c6e3d432 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_pin_code": "Kode PIN Anda masukkan tidak valid" + }, + "step": { + "pairing": { + "data": { + "pin": "Kode PIN" + }, + "description": "Masukkan Kode PIN yang ditampilkan di TV Anda", + "title": "Memasangkan" + }, + "user": { + "data": { + "host": "Alamat IP", + "name": "Nama" + }, + "description": "Masukkan Alamat IP TV Panasonic Viera Anda", + "title": "Siapkan TV Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/ko.json b/homeassistant/components/panasonic_viera/translations/ko.json index 0f3252a4ab1..4eb136c1149 100644 --- a/homeassistant/components/panasonic_viera/translations/ko.json +++ b/homeassistant/components/panasonic_viera/translations/ko.json @@ -14,7 +14,7 @@ "data": { "pin": "PIN \ucf54\ub4dc" }, - "description": "TV \uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "description": "TV\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "\ud398\uc5b4\ub9c1\ud558\uae30" }, "user": { @@ -22,7 +22,7 @@ "host": "IP \uc8fc\uc18c", "name": "\uc774\ub984" }, - "description": "Panasonic Viera TV \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "description": "Panasonic Viera TV\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "TV \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/panasonic_viera/translations/nl.json b/homeassistant/components/panasonic_viera/translations/nl.json index 96757370bed..fa63892730e 100644 --- a/homeassistant/components/panasonic_viera/translations/nl.json +++ b/homeassistant/components/panasonic_viera/translations/nl.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "Deze Panasonic Viera TV is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "unknown": "Er is een onbekende fout opgetreden. Controleer de logs voor meer informatie." + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_pin_code": "De ingevoerde pincode is ongeldig" + "invalid_pin_code": "De PIN-code die u hebt ingevoerd is ongeldig" }, "step": { "pairing": { "data": { - "pin": "PIN" + "pin": "PIN-code" }, "description": "Voer de PIN-code in die op uw TV wordt weergegeven", "title": "Koppelen" @@ -22,7 +22,7 @@ "host": "IP-adres", "name": "Naam" }, - "description": "Voer het IP-adres van uw Panasonic Viera TV in", + "description": "Voer de IP-adres van uw Panasonic Viera TV in.", "title": "Uw tv instellen" } } diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 7f193bc09a1..5621846e496 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -93,6 +93,6 @@ class PencomRelay(SwitchEntity): self._state = self._hub.get(self._board, self._addr) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return supported attributes.""" return {"board": self._board, "addr": self._addr} diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 589cc97baea..05d52cf7830 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,7 +1,9 @@ """Support for displaying persistent notifications.""" +from __future__ import annotations + from collections import OrderedDict import logging -from typing import Any, Mapping, MutableMapping, Optional +from typing import Any, Mapping, MutableMapping import voluptuous as vol @@ -71,8 +73,8 @@ def dismiss(hass, notification_id): def async_create( hass: HomeAssistant, message: str, - title: Optional[str] = None, - notification_id: Optional[str] = None, + title: str | None = None, + notification_id: str | None = None, ) -> None: """Generate a notification.""" data = { diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index d3e17d904ea..1eb9d4eda7a 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -1,6 +1,8 @@ """Support for tracking people.""" +from __future__ import annotations + import logging -from typing import List, Optional, cast +from typing import cast import voluptuous as vol @@ -171,7 +173,7 @@ class PersonStorageCollection(collection.StorageCollection): super().__init__(store, logger, id_manager) self.yaml_collection = yaml_collection - async def _async_load_data(self) -> Optional[dict]: + async def _async_load_data(self) -> dict | None: """Load the data. A past bug caused onboarding to create invalid person objects. @@ -257,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: HomeAssistantType, persons: list[dict]) -> list[dict]: """Validate YAML data that we can't validate via schema.""" filtered = [] person_invalid_user = [] @@ -265,16 +267,15 @@ async def filter_yaml_data(hass: HomeAssistantType, persons: List[dict]) -> List for person_conf in persons: user_id = person_conf.get(CONF_USER_ID) - if user_id is not None: - if await hass.auth.async_get_user(user_id) is None: - _LOGGER.error( - "Invalid user_id detected for person %s", - person_conf[collection.CONF_ID], - ) - person_invalid_user.append( - f"- Person {person_conf[CONF_NAME]} (id: {person_conf[collection.CONF_ID]}) points at invalid user {user_id}" - ) - continue + if user_id is not None and await hass.auth.async_get_user(user_id) is None: + _LOGGER.error( + "Invalid user_id detected for person %s", + person_conf[collection.CONF_ID], + ) + person_invalid_user.append( + f"- Person {person_conf[CONF_NAME]} (id: {person_conf[collection.CONF_ID]}) points at invalid user {user_id}" + ) + continue filtered.append(person_conf) @@ -380,7 +381,7 @@ class Person(RestoreEntity): return self._config[CONF_NAME] @property - def entity_picture(self) -> Optional[str]: + def entity_picture(self) -> str | None: """Return entity picture.""" return self._config.get(CONF_PICTURE) @@ -398,7 +399,7 @@ class Person(RestoreEntity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the person.""" data = {ATTR_EDITABLE: self.editable, ATTR_ID: self.unique_id} if self._latitude is not None: @@ -522,7 +523,7 @@ def ws_list_person( ) -def _get_latest(prev: Optional[State], curr: State): +def _get_latest(prev: State | None, curr: State): """Get latest state.""" if prev is None or curr.last_updated > prev.last_updated: return curr diff --git a/homeassistant/components/person/significant_change.py b/homeassistant/components/person/significant_change.py index d9c1ec6cc23..680b9194144 100644 --- a/homeassistant/components/person/significant_change.py +++ b/homeassistant/components/person/significant_change.py @@ -1,5 +1,7 @@ """Helper to test significant Person state changes.""" -from typing import Any, Optional +from __future__ import annotations + +from typing import Any from homeassistant.core import HomeAssistant, callback @@ -12,7 +14,7 @@ def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs: Any, -) -> Optional[bool]: +) -> bool | None: """Test if state significantly changed.""" if new_state != old_state: diff --git a/homeassistant/components/person/translations/id.json b/homeassistant/components/person/translations/id.json index 2be3be8476a..1535bb2bd50 100644 --- a/homeassistant/components/person/translations/id.json +++ b/homeassistant/components/person/translations/id.json @@ -1,7 +1,7 @@ { "state": { "_": { - "home": "Di rumah", + "home": "Di Rumah", "not_home": "Keluar" } }, diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index f3c2eb59789..7be5efeaf2f 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -1,8 +1,10 @@ """The Philips TV integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable from haphilipsjs import ConnectionFailure, PhilipsTV @@ -21,7 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN -PLATFORMS = ["media_player"] +PLATFORMS = ["media_player", "remote"] LOGGER = logging.getLogger(__name__) @@ -47,9 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -60,8 +62,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -77,14 +79,14 @@ class PluggableAction: def __init__(self, update: Callable[[], None]): """Initialize.""" self._update = update - self._actions: Dict[Any, AutomationActionType] = {} + self._actions: dict[Any, AutomationActionType] = {} def __bool__(self): """Return if we have something attached.""" return bool(self._actions) @callback - def async_attach(self, action: AutomationActionType, variables: Dict[str, Any]): + def async_attach(self, action: AutomationActionType, variables: dict[str, Any]): """Attach a device trigger for turn on.""" @callback @@ -99,9 +101,7 @@ class PluggableAction: return _remove - async def async_run( - self, hass: HomeAssistantType, context: Optional[Context] = None - ): + async def async_run(self, hass: HomeAssistantType, context: Context | None = None): """Run all turn on triggers.""" for job, variables in self._actions.values(): hass.async_run_hass_job(job, variables, context) @@ -113,7 +113,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): def __init__(self, hass, api: PhilipsTV) -> None: """Set up the coordinator.""" self.api = api - self._notify_future: Optional[asyncio.Task] = None + self._notify_future: asyncio.Task | None = None @callback def _update_listeners(): @@ -150,7 +150,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): and self.api.on and self.api.notify_change_supported ): - self._notify_future = self.hass.loop.create_task(self._notify_task()) + self._notify_future = asyncio.create_task(self._notify_task()) @callback def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 778bcba282b..8f0bcd161fc 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Philips TV integration.""" +from __future__ import annotations + import platform -from typing import Any, Dict, Optional, Tuple, TypedDict +from typing import Any from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV import voluptuous as vol @@ -15,23 +17,12 @@ from homeassistant.const import ( ) from . import LOGGER -from .const import ( # pylint:disable=unused-import - CONF_SYSTEM, - CONST_APP_ID, - CONST_APP_NAME, - DOMAIN, -) - - -class FlowUserDict(TypedDict): - """Data for user step.""" - - host: str +from .const import CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN async def validate_input( hass: core.HomeAssistant, host: str, api_version: int -) -> Tuple[Dict, PhilipsTV]: +) -> tuple[dict, PhilipsTV]: """Validate the user input allows us to connect.""" hub = PhilipsTV(host, api_version) @@ -50,11 +41,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - _current = {} - _hub: PhilipsTV - _pair_state: Any + def __init__(self) -> None: + """Initialize flow.""" + super().__init__() + self._current = {} + self._hub: PhilipsTV | None = None + self._pair_state: Any = None - async def async_step_import(self, conf: Dict[str, Any]): + async def async_step_import(self, conf: dict) -> dict: """Import a configuration from config.yaml.""" for entry in self._async_current_entries(): if entry.data[CONF_HOST] == conf[CONF_HOST]: @@ -75,7 +69,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data=self._current, ) - async def async_step_pair(self, user_input: Optional[Dict] = None): + async def async_step_pair(self, user_input: dict | None = None) -> dict: """Attempt to pair with device.""" assert self._hub @@ -96,7 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "native", ) except PairingFailure as exc: - LOGGER.debug(str(exc)) + LOGGER.debug(exc) return self.async_abort( reason="pairing_failure", description_placeholders={"error_id": exc.data.get("error_id")}, @@ -110,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._pair_state, user_input[CONF_PIN] ) except PairingFailure as exc: - LOGGER.debug(str(exc)) + LOGGER.debug(exc) if exc.data.get("error_id") == "INVALID_PIN": errors[CONF_PIN] = "invalid_pin" return self.async_show_form( @@ -126,7 +120,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._current[CONF_PASSWORD] = password return await self._async_create_current() - async def async_step_user(self, user_input: Optional[FlowUserDict] = None): + async def async_step_user(self, user_input: dict | None = None) -> dict: """Handle the initial step.""" errors = {} if user_input: @@ -136,7 +130,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION] ) except ConnectionFailure as exc: - LOGGER.error(str(exc)) + LOGGER.error(exc) errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 2a60a1664bc..77782fc641c 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for control of device.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -23,7 +23,7 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for device.""" triggers = [] triggers.append( @@ -43,8 +43,9 @@ async def async_attach_trigger( config: ConfigType, action: AutomationActionType, automation_info: dict, -) -> Optional[CALLBACK_TYPE]: +) -> CALLBACK_TYPE | None: """Attach a trigger.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None registry: DeviceRegistry = await async_get_registry(hass) if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON: variables = { @@ -53,6 +54,7 @@ async def async_attach_trigger( "domain": DOMAIN, "device_id": config[CONF_DEVICE_ID], "description": f"philips_js '{config[CONF_TYPE]}' event", + "id": trigger_id, } } diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 9ed1cedbf05..ad591ad330b 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -3,7 +3,7 @@ "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ - "ha-philipsjs==2.3.1" + "ha-philipsjs==2.3.2" ], "codeowners": [ "@elupus" diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 2b2714b20ce..7376d34e308 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,5 +1,7 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any from haphilipsjs import ConnectionFailure import voluptuous as vol @@ -125,7 +127,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, - system: Dict[str, Any], + system: dict[str, Any], unique_id: str, ): """Initialize the Philips TV.""" @@ -137,10 +139,10 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): self._system = system self._unique_id = unique_id self._state = STATE_OFF - self._media_content_type: Optional[str] = None - self._media_content_id: Optional[str] = None - self._media_title: Optional[str] = None - self._media_channel: Optional[str] = None + self._media_content_type: str | None = None + self._media_content_id: str | None = None + self._media_title: str | None = None + self._media_channel: str | None = None super().__init__(coordinator) self._update_from_coordinator() @@ -168,9 +170,8 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def state(self): """Get the device state. An exception means OFF state.""" - if self._tv.on: - if self._tv.powerstate == "On" or self._tv.powerstate is None: - return STATE_ON + if self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None): + return STATE_ON return STATE_OFF @property @@ -295,8 +296,8 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): def media_image_url(self): """Image url of current playing media.""" if self._media_content_id and self._media_content_type in ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, + MEDIA_TYPE_APP, + MEDIA_TYPE_CHANNEL, ): return self.get_browse_image_url( self._media_content_type, self._media_content_id, media_image_id=None @@ -370,9 +371,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, can_expand=False, - thumbnail=self.get_browse_image_url( - MEDIA_TYPE_APP, channel_id, media_image_id=None - ), ) for channel_id, channel in self._tv.channels.items() ] @@ -384,7 +382,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): media_class=MEDIA_CLASS_DIRECTORY, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_TYPE_CHANNEL, + children_media_class=MEDIA_CLASS_CHANNEL, can_play=False, can_expand=True, children=children, @@ -410,9 +408,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, can_expand=False, - thumbnail=self.get_browse_image_url( - MEDIA_TYPE_APP, channel, media_image_id=None - ), ) for channel in favorites ] @@ -427,7 +422,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): media_class=MEDIA_CLASS_DIRECTORY, media_content_id=f"favorites/{list_id}", media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_TYPE_CHANNEL, + children_media_class=MEDIA_CLASS_CHANNEL, can_play=False, can_expand=True, children=children, @@ -458,7 +453,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): media_class=MEDIA_CLASS_DIRECTORY, media_content_id="applications", media_content_type=MEDIA_TYPE_APPS, - children_media_class=MEDIA_TYPE_APP, + children_media_class=MEDIA_CLASS_APP, can_play=False, can_expand=True, children=children, @@ -479,7 +474,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): media_class=MEDIA_CLASS_DIRECTORY, media_content_id="favorite_lists", media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_TYPE_CHANNEL, + children_media_class=MEDIA_CLASS_CHANNEL, can_play=False, can_expand=True, children=children, diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py new file mode 100644 index 00000000000..f4d34904f1b --- /dev/null +++ b/homeassistant/components/philips_js/remote.py @@ -0,0 +1,107 @@ +"""Remote control support for Apple TV.""" + +import asyncio + +from haphilipsjs.typing import SystemType + +from homeassistant.components.remote import ( + ATTR_DELAY_SECS, + ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, + RemoteEntity, +) + +from . import LOGGER, PhilipsTVDataUpdateCoordinator +from .const import CONF_SYSTEM, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the configuration entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + PhilipsTVRemote( + coordinator, + config_entry.data[CONF_SYSTEM], + config_entry.unique_id, + ) + ] + ) + + +class PhilipsTVRemote(RemoteEntity): + """Device that sends commands.""" + + def __init__( + self, + coordinator: PhilipsTVDataUpdateCoordinator, + system: SystemType, + unique_id: str, + ): + """Initialize the Philips TV.""" + self._tv = coordinator.api + self._coordinator = coordinator + self._system = system + self._unique_id = unique_id + + @property + def name(self): + """Return the device name.""" + return self._system["name"] + + @property + def is_on(self): + """Return true if device is on.""" + return bool( + self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None) + ) + + @property + def should_poll(self): + """No polling needed for Apple TV.""" + return False + + @property + def unique_id(self): + """Return unique identifier if known.""" + return self._unique_id + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "name": self._system["name"], + "identifiers": { + (DOMAIN, self._unique_id), + }, + "model": self._system.get("model"), + "manufacturer": "Philips", + "sw_version": self._system.get("softwareversion"), + } + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + if self._tv.on and self._tv.powerstate: + await self._tv.setPowerState("On") + else: + await self._coordinator.turn_on.async_run(self.hass, self._context) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + if self._tv.on: + await self._tv.sendKey("Standby") + self.async_write_ha_state() + else: + LOGGER.debug("Tv was already turned off") + + async def async_send_command(self, command, **kwargs): + """Send a command to one device.""" + num_repeats = kwargs[ATTR_NUM_REPEATS] + delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + + for _ in range(num_repeats): + for single_command in command: + LOGGER.debug("Sending command %s", single_command) + await self._tv.sendKey(single_command) + await asyncio.sleep(delay) diff --git a/homeassistant/components/philips_js/strings.json b/homeassistant/components/philips_js/strings.json index df65d453f2b..5c8f08eff6a 100644 --- a/homeassistant/components/philips_js/strings.json +++ b/homeassistant/components/philips_js/strings.json @@ -6,6 +6,13 @@ "host": "[%key:common::config_flow::data::host%]", "api_version": "API Version" } + }, + "pair": { + "title": "Pair", + "description": "Enter the PIN displayed on your TV", + "data":{ + "pin": "[%key:common::config_flow::data::pin%]" + } } }, "error": { diff --git a/homeassistant/components/philips_js/translations/bg.json b/homeassistant/components/philips_js/translations/bg.json new file mode 100644 index 00000000000..eb502f5a135 --- /dev/null +++ b/homeassistant/components/philips_js/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_pin": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u041f\u0418\u041d", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ca.json b/homeassistant/components/philips_js/translations/ca.json index 980bb6800e1..b94faccd615 100644 --- a/homeassistant/components/philips_js/translations/ca.json +++ b/homeassistant/components/philips_js/translations/ca.json @@ -10,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "pair": { + "data": { + "pin": "Codi PIN" + }, + "description": "Introdueix el PIN que es mostra al televisor", + "title": "Vinculaci\u00f3" + }, "user": { "data": { "api_version": "Versi\u00f3 de l'API", diff --git a/homeassistant/components/philips_js/translations/cs.json b/homeassistant/components/philips_js/translations/cs.json index a39944a8dba..57ba2a219b6 100644 --- a/homeassistant/components/philips_js/translations/cs.json +++ b/homeassistant/components/philips_js/translations/cs.json @@ -8,6 +8,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "pair": { + "data": { + "pin": "PIN k\u00f3d" + } + }, "user": { "data": { "api_version": "Verze API", diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json index f59a17bce49..6288e9fb5c4 100644 --- a/homeassistant/components/philips_js/translations/de.json +++ b/homeassistant/components/philips_js/translations/de.json @@ -6,9 +6,16 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_pin": "Ung\u00fcltige PIN", + "pairing_failure": "Fehler beim Koppeln: {error_id}", "unknown": "Unerwarteter Fehler" }, "step": { + "pair": { + "data": { + "pin": "PIN-Code" + }, + "description": "Gib die auf deinem Fernseher angezeigten PIN ein" + }, "user": { "data": { "api_version": "API-Version", @@ -16,5 +23,10 @@ } } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Ger\u00e4t wird zum Einschalten aufgefordert" + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/el.json b/homeassistant/components/philips_js/translations/el.json new file mode 100644 index 00000000000..94f6b540bd0 --- /dev/null +++ b/homeassistant/components/philips_js/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "pair": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2", + "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/en.json b/homeassistant/components/philips_js/translations/en.json index 65d4f417b9f..ea254a3873d 100644 --- a/homeassistant/components/philips_js/translations/en.json +++ b/homeassistant/components/philips_js/translations/en.json @@ -10,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "pair": { + "data": { + "pin": "PIN Code" + }, + "description": "Enter the PIN displayed on your TV", + "title": "Pair" + }, "user": { "data": { "api_version": "API Version", diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index d4476f29981..c8d34e9ea9d 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "invalid_pin": "PIN no v\u00e1lido", + "pairing_failure": "No se ha podido emparejar: {error_id}" + }, "step": { + "pair": { + "description": "Introduzca el PIN que se muestra en el televisor", + "title": "Par" + }, "user": { "data": { "api_version": "Versi\u00f3n del API", diff --git a/homeassistant/components/philips_js/translations/et.json b/homeassistant/components/philips_js/translations/et.json index 9953df9c272..4a5bf9fe6e9 100644 --- a/homeassistant/components/philips_js/translations/et.json +++ b/homeassistant/components/philips_js/translations/et.json @@ -10,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "pair": { + "data": { + "pin": "PIN kood" + }, + "description": "Sisesta teleris kuvatav PIN-kood", + "title": "Paarita" + }, "user": { "data": { "api_version": "API versioon", diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json index 25c28edcf1d..eb16bb92271 100644 --- a/homeassistant/components/philips_js/translations/fr.json +++ b/homeassistant/components/philips_js/translations/fr.json @@ -10,6 +10,13 @@ "unknown": "Erreur inattendue" }, "step": { + "pair": { + "data": { + "pin": "Code PIN" + }, + "description": "Entrez le code PIN affich\u00e9 sur votre t\u00e9l\u00e9viseur", + "title": "Appairer" + }, "user": { "data": { "api_version": "Version de l'API", diff --git a/homeassistant/components/philips_js/translations/hu.json b/homeassistant/components/philips_js/translations/hu.json new file mode 100644 index 00000000000..f7ce3f708b0 --- /dev/null +++ b/homeassistant/components/philips_js/translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_pin": "\u00c9rv\u00e9nytelen PIN", + "pairing_failure": "Nem lehet p\u00e1ros\u00edtani: {error_id}", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "pair": { + "data": { + "pin": "PIN-k\u00f3d" + }, + "description": "\u00cdrd be a t\u00e9v\u00e9n megjelen\u0151 PIN-k\u00f3dot", + "title": "P\u00e1ros\u00edt\u00e1s" + }, + "user": { + "data": { + "api_version": "API Verzi\u00f3", + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/id.json b/homeassistant/components/philips_js/translations/id.json new file mode 100644 index 00000000000..633cfdd633e --- /dev/null +++ b/homeassistant/components/philips_js/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_pin": "PIN tidak valid", + "pairing_failure": "Tidak dapat memasangkan: {error_id}", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_version": "Versi API", + "host": "Host" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Perangkat diminta untuk dinyalakan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/it.json b/homeassistant/components/philips_js/translations/it.json index 216248d8eac..6ff668dbea8 100644 --- a/homeassistant/components/philips_js/translations/it.json +++ b/homeassistant/components/philips_js/translations/it.json @@ -5,9 +5,18 @@ }, "error": { "cannot_connect": "Impossibile connettersi", + "invalid_pin": "PIN non valido", + "pairing_failure": "Impossibile eseguire l'associazione: {error_id}", "unknown": "Errore imprevisto" }, "step": { + "pair": { + "data": { + "pin": "Codice PIN" + }, + "description": "Inserire il PIN visualizzato sul televisore", + "title": "Associa" + }, "user": { "data": { "api_version": "Versione API", diff --git a/homeassistant/components/philips_js/translations/ko.json b/homeassistant/components/philips_js/translations/ko.json index 85281856809..04ba5eff601 100644 --- a/homeassistant/components/philips_js/translations/ko.json +++ b/homeassistant/components/philips_js/translations/ko.json @@ -5,14 +5,29 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "pairing_failure": "\ud398\uc5b4\ub9c1\uc744 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4: {error_id}", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "pair": { + "data": { + "pin": "PIN \ucf54\ub4dc" + }, + "description": "TV\uc5d0 \ud45c\uc2dc\ub41c PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "\ud398\uc5b4\ub9c1\ud558\uae30" + }, "user": { "data": { + "api_version": "API \ubc84\uc804", "host": "\ud638\uc2a4\ud2b8" } } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\uae30\uae30\uac00 \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/nl.json b/homeassistant/components/philips_js/translations/nl.json index 23cd7e47043..34497d285fa 100644 --- a/homeassistant/components/philips_js/translations/nl.json +++ b/homeassistant/components/philips_js/translations/nl.json @@ -5,9 +5,18 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", + "invalid_pin": "Ongeldige pincode", + "pairing_failure": "Kan niet koppelen: {error_id}", "unknown": "Onverwachte fout" }, "step": { + "pair": { + "data": { + "pin": "PIN-code" + }, + "description": "Voer de pincode in die op uw tv wordt weergegeven", + "title": "Koppel" + }, "user": { "data": { "api_version": "API Versie", @@ -15,5 +24,10 @@ } } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Apparaat wordt gevraagd om in te schakelen" + } } } \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/no.json b/homeassistant/components/philips_js/translations/no.json index a9c647a644b..5b1df7c8e8b 100644 --- a/homeassistant/components/philips_js/translations/no.json +++ b/homeassistant/components/philips_js/translations/no.json @@ -10,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "pair": { + "data": { + "pin": "PIN kode" + }, + "description": "Angi PIN-koden som vises p\u00e5 TV-en", + "title": "Par" + }, "user": { "data": { "api_version": "API-versjon", diff --git a/homeassistant/components/philips_js/translations/pl.json b/homeassistant/components/philips_js/translations/pl.json index 27c088350c4..a89b6136ff8 100644 --- a/homeassistant/components/philips_js/translations/pl.json +++ b/homeassistant/components/philips_js/translations/pl.json @@ -5,9 +5,18 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_pin": "Nieprawid\u0142owy kod PIN", + "pairing_failure": "Nie mo\u017cna sparowa\u0107: {error_id}", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "pair": { + "data": { + "pin": "Kod PIN" + }, + "description": "Wprowad\u017a kod PIN wy\u015bwietlony na Twoim telewizorze", + "title": "Paruj" + }, "user": { "data": { "api_version": "Wersja API", diff --git a/homeassistant/components/philips_js/translations/pt.json b/homeassistant/components/philips_js/translations/pt.json new file mode 100644 index 00000000000..4646fcae7dc --- /dev/null +++ b/homeassistant/components/philips_js/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_pin": "PIN inv\u00e1lido" + }, + "step": { + "pair": { + "data": { + "pin": "C\u00f3digo PIN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/ru.json b/homeassistant/components/philips_js/translations/ru.json index 83511ff246a..df3dfd4b6f6 100644 --- a/homeassistant/components/philips_js/translations/ru.json +++ b/homeassistant/components/philips_js/translations/ru.json @@ -10,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "pair": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + }, "user": { "data": { "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f API", diff --git a/homeassistant/components/philips_js/translations/zh-Hans.json b/homeassistant/components/philips_js/translations/zh-Hans.json new file mode 100644 index 00000000000..1353d8d1225 --- /dev/null +++ b/homeassistant/components/philips_js/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_pin": "\u65e0\u6548\u7684PIN\u7801" + } + } +} \ 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 13bfd52e980..7ae9c8893d5 100644 --- a/homeassistant/components/philips_js/translations/zh-Hant.json +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -10,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "pair": { + "data": { + "pin": "PIN \u78bc" + }, + "description": "\u8f38\u5165\u96fb\u8996\u986f\u793a\u4e4b PIN \u78bc", + "title": "\u914d\u5c0d" + }, "user": { "data": { "api_version": "API \u7248\u672c", diff --git a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py index 61cedb8f063..bdec7714eef 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py +++ b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py @@ -1,5 +1,5 @@ """Support for binary sensor using RPi GPIO.""" -from pi4ioe5v9xxxx import pi4ioe5v9xxxx # pylint: disable=import-error +from pi4ioe5v9xxxx import pi4ioe5v9xxxx import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity diff --git a/homeassistant/components/pi4ioe5v9xxxx/switch.py b/homeassistant/components/pi4ioe5v9xxxx/switch.py index 81de76c086c..85bde509070 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/switch.py +++ b/homeassistant/components/pi4ioe5v9xxxx/switch.py @@ -1,5 +1,5 @@ """Allows to configure a switch using RPi GPIO.""" -from pi4ioe5v9xxxx import pi4ioe5v9xxxx # pylint: disable=import-error +from pi4ioe5v9xxxx import pi4ioe5v9xxxx import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 2d540d936e5..bc486a0c901 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass, entry): try: await api.get_data() except HoleError as err: - raise UpdateFailed(f"Failed to communicating with API: {err}") from err + raise UpdateFailed(f"Failed to communicate with API: {err}") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index a7d4b387b1c..60d53c4f904 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -6,7 +6,7 @@ from hole.exceptions import HoleError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.pi_hole.const import ( # pylint: disable=unused-import +from homeassistant.components.pi_hole.const import ( CONF_LOCATION, CONF_STATISTICS_ONLY, DEFAULT_LOCATION, diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 2f5873b14c1..517e8cfcf17 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,5 +1,6 @@ """Support for getting statistical data from a Pi-hole system.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME from . import PiHoleEntity @@ -30,7 +31,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors, True) -class PiHoleSensor(PiHoleEntity): +class PiHoleSensor(PiHoleEntity, SensorEntity): """Representation of a Pi-hole sensor.""" def __init__(self, api, coordinator, name, sensor_name, server_unique_id): @@ -73,6 +74,6 @@ class PiHoleSensor(PiHoleEntity): return self.api.data[self._condition] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Pi-hole.""" return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]} diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index f1bd9a106bc..a8f8563da41 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -7,10 +7,21 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "api_key": { + "data": { + "api_key": "API kulcs" + } + }, "user": { "data": { + "api_key": "API kulcs", "host": "Hoszt", - "port": "Port" + "location": "Elhelyezked\u00e9s", + "name": "N\u00e9v", + "port": "Port", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "statistics_only": "Csak statisztik\u00e1k", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } } } diff --git a/homeassistant/components/pi_hole/translations/id.json b/homeassistant/components/pi_hole/translations/id.json new file mode 100644 index 00000000000..c38c0f5c9bb --- /dev/null +++ b/homeassistant/components/pi_hole/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "api_key": { + "data": { + "api_key": "Kunci API" + } + }, + "user": { + "data": { + "api_key": "Kunci API", + "host": "Host", + "location": "Lokasi", + "name": "Nama", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "statistics_only": "Hanya Statistik", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json index 7261742b2a6..d79878d8a42 100644 --- a/homeassistant/components/pi_hole/translations/ko.json +++ b/homeassistant/components/pi_hole/translations/ko.json @@ -20,6 +20,7 @@ "name": "\uc774\ub984", "port": "\ud3ec\ud2b8", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", + "statistics_only": "\ud1b5\uacc4 \uc804\uc6a9", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" } } diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index e8c7b4bd4b6..97458acd5fc 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -4,10 +4,9 @@ import logging import voluptuous as vol from homeassistant.components import pilight -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -39,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class PilightSensor(Entity): +class PilightSensor(SensorEntity): """Representation of a sensor that can be updated using Pilight.""" def __init__(self, hass, name, variable, payload, unit_of_measurement): diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index 19207ada1b7..726bb212574 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -1,13 +1,26 @@ """The ping component.""" +from __future__ import annotations + +import logging + +from icmplib import SocketPermissionError, ping as icmp_ping from homeassistant.core import callback +from homeassistant.helpers.reload import async_setup_reload_service -DOMAIN = "ping" -PLATFORMS = ["binary_sensor"] +from .const import DEFAULT_START_ID, DOMAIN, MAX_PING_ID, PING_ID, PING_PRIVS, PLATFORMS -PING_ID = "ping_id" -DEFAULT_START_ID = 129 -MAX_PING_ID = 65534 +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the template integration.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + hass.data[DOMAIN] = { + PING_PRIVS: await hass.async_add_executor_job(_can_use_icmp_lib_with_privilege), + PING_ID: DEFAULT_START_ID, + } + return True @callback @@ -16,8 +29,7 @@ def async_get_next_ping_id(hass): Must be called in async """ - current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID) - + current_id = hass.data[DOMAIN][PING_ID] if current_id == MAX_PING_ID: next_id = DEFAULT_START_ID else: @@ -26,3 +38,23 @@ def async_get_next_ping_id(hass): hass.data[DOMAIN][PING_ID] = next_id return next_id + + +def _can_use_icmp_lib_with_privilege() -> None | bool: + """Verify we can create a raw socket.""" + try: + icmp_ping("127.0.0.1", count=0, timeout=0, privileged=True) + except SocketPermissionError: + try: + icmp_ping("127.0.0.1", count=0, timeout=0, privileged=False) + except SocketPermissionError: + _LOGGER.debug( + "Cannot use icmplib because privileges are insufficient to create the socket" + ) + return None + else: + _LOGGER.debug("Using icmplib in privileged=False mode") + return False + else: + _LOGGER.debug("Using icmplib in privileged=True mode") + return True diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 98c36c01d98..9ae891d598b 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,13 +1,16 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" +from __future__ import annotations + import asyncio +from contextlib import suppress from datetime import timedelta from functools import partial import logging import re import sys -from typing import Any, Dict +from typing import Any -from icmplib import SocketPermissionError, ping as icmp_ping +from icmplib import NameLookupError, ping as icmp_ping import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -15,12 +18,12 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_ON import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity -from . import DOMAIN, PLATFORMS, async_get_next_ping_id -from .const import PING_TIMEOUT +from . import async_get_next_ping_id +from .const import DOMAIN, ICMP_TIMEOUT, PING_PRIVS, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -60,32 +63,30 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None) -> None: +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the Ping Binary sensor.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) - host = config[CONF_HOST] count = config[CONF_PING_COUNT] name = config.get(CONF_NAME, f"{DEFAULT_NAME} {host}") - - try: - # Verify we can create a raw socket, or - # fallback to using a subprocess - icmp_ping("127.0.0.1", count=0, timeout=0) - ping_cls = PingDataICMPLib - except SocketPermissionError: + privileged = hass.data[DOMAIN][PING_PRIVS] + if privileged is None: ping_cls = PingDataSubProcess + else: + ping_cls = PingDataICMPLib - ping_data = ping_cls(hass, host, count) - - add_entities([PingBinarySensor(name, ping_data)], True) + async_add_entities( + [PingBinarySensor(name, ping_cls(hass, host, count, privileged))] + ) -class PingBinarySensor(BinarySensorEntity): +class PingBinarySensor(RestoreEntity, BinarySensorEntity): """Representation of a Ping Binary sensor.""" def __init__(self, name: str, ping) -> None: """Initialize the Ping Binary sensor.""" + self._available = False self._name = name self._ping = ping @@ -94,6 +95,11 @@ class PingBinarySensor(BinarySensorEntity): """Return the name of the device.""" return self._name + @property + def available(self) -> str: + """Return if we have done the first ping.""" + return self._available + @property def device_class(self) -> str: """Return the class of this sensor.""" @@ -102,10 +108,10 @@ class PingBinarySensor(BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self._ping.available + return self._ping.is_alive @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the ICMP checo request.""" if self._ping.data is not False: return { @@ -118,6 +124,28 @@ class PingBinarySensor(BinarySensorEntity): async def async_update(self) -> None: """Get the latest data.""" await self._ping.async_update() + self._available = True + + async def async_added_to_hass(self): + """Restore previous state on restart to avoid blocking startup.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if last_state is not None: + self._available = True + + if last_state is None or last_state.state != STATE_ON: + self._ping.data = False + return + + attributes = last_state.attributes + self._ping.is_alive = True + self._ping.data = { + "min": attributes[ATTR_ROUND_TRIP_TIME_AVG], + "max": attributes[ATTR_ROUND_TRIP_TIME_MAX], + "avg": attributes[ATTR_ROUND_TRIP_TIME_MDEV], + "mdev": attributes[ATTR_ROUND_TRIP_TIME_MIN], + } class PingData: @@ -129,26 +157,37 @@ class PingData: self._ip_address = host self._count = count self.data = {} - self.available = False + self.is_alive = False class PingDataICMPLib(PingData): """The Class for handling the data retrieval using icmplib.""" + def __init__(self, hass, host, count, privileged) -> None: + """Initialize the data object.""" + super().__init__(hass, host, count) + self._privileged = privileged + async def async_update(self) -> None: """Retrieve the latest details from the host.""" _LOGGER.debug("ping address: %s", self._ip_address) - data = await self.hass.async_add_executor_job( - partial( - icmp_ping, - self._ip_address, - count=self._count, - timeout=1, - id=async_get_next_ping_id(self.hass), + try: + data = await self.hass.async_add_executor_job( + partial( + icmp_ping, + self._ip_address, + count=self._count, + timeout=ICMP_TIMEOUT, + id=async_get_next_ping_id(self.hass), + privileged=self._privileged, + ) ) - ) - self.available = data.is_alive - if not self.available: + except NameLookupError: + self.is_alive = False + return + + self.is_alive = data.is_alive + if not self.is_alive: self.data = False return @@ -163,7 +202,7 @@ class PingDataICMPLib(PingData): class PingDataSubProcess(PingData): """The Class for handling the data retrieval using the ping binary.""" - def __init__(self, hass, host, count) -> None: + def __init__(self, hass, host, count, privileged) -> None: """Initialize the data object.""" super().__init__(hass, host, count) if sys.platform == "win32": @@ -240,10 +279,8 @@ class PingDataSubProcess(PingData): self._count + PING_TIMEOUT, ) if pinger: - try: + with suppress(TypeError): await pinger.kill() - except TypeError: - pass del pinger return False @@ -253,4 +290,4 @@ class PingDataSubProcess(PingData): async def async_update(self) -> None: """Retrieve the latest details from the host.""" self.data = await self.async_ping() - self.available = bool(self.data) + self.is_alive = bool(self.data) diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py index 89b93c84169..62fca9123ba 100644 --- a/homeassistant/components/ping/const.py +++ b/homeassistant/components/ping/const.py @@ -1,4 +1,21 @@ """Tracks devices by sending a ICMP echo request (ping).""" +# The ping binary and icmplib timeouts are not the same +# timeout. ping is an overall timeout, icmplib is the +# time since the data was sent. + +# ping binary PING_TIMEOUT = 3 + +# icmplib timeout +ICMP_TIMEOUT = 1 + PING_ATTEMPTS_COUNT = 3 + +DOMAIN = "ping" +PLATFORMS = ["binary_sensor"] + +PING_ID = "ping_id" +PING_PRIVS = "ping_privs" +DEFAULT_START_ID = 129 +MAX_PING_ID = 65534 diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 4139438bcfe..a6b75a9245b 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,10 +1,12 @@ """Tracks devices by sending a ICMP echo request (ping).""" +import asyncio from datetime import timedelta +from functools import partial import logging import subprocess import sys -from icmplib import SocketPermissionError, ping as icmp_ping +from icmplib import multiping import voluptuous as vol from homeassistant import const, util @@ -15,16 +17,18 @@ from homeassistant.components.device_tracker.const import ( SOURCE_TYPE_ROUTER, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.process import kill_subprocess from . import async_get_next_ping_id -from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT +from .const import DOMAIN, ICMP_TIMEOUT, PING_ATTEMPTS_COUNT, PING_PRIVS, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 CONF_PING_COUNT = "count" +CONCURRENT_PING_LIMIT = 6 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -37,16 +41,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( class HostSubProcess: """Host object with ping detection.""" - def __init__(self, ip_address, dev_id, hass, config): + def __init__(self, ip_address, dev_id, hass, config, privileged): """Initialize the Host pinger.""" self.hass = hass self.ip_address = ip_address self.dev_id = dev_id self._count = config[CONF_PING_COUNT] if sys.platform == "win32": - self._ping_cmd = ["ping", "-n", "1", "-w", "1000", self.ip_address] + self._ping_cmd = ["ping", "-n", "1", "-w", "1000", ip_address] else: - self._ping_cmd = ["ping", "-n", "-q", "-c1", "-W1", self.ip_address] + self._ping_cmd = ["ping", "-n", "-q", "-c1", "-W1", ip_address] def ping(self): """Send an ICMP echo request and return True if success.""" @@ -63,86 +67,83 @@ class HostSubProcess: except subprocess.CalledProcessError: return False - def update(self, see): + def update(self) -> bool: """Update device state by sending one or more ping messages.""" failed = 0 while failed < self._count: # check more times if host is unreachable if self.ping(): - see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER) return True failed += 1 _LOGGER.debug("No response from %s failed=%d", self.ip_address, failed) + return False -class HostICMPLib: - """Host object with ping detection.""" - - def __init__(self, ip_address, dev_id, hass, config): - """Initialize the Host pinger.""" - self.hass = hass - self.ip_address = ip_address - self.dev_id = dev_id - self._count = config[CONF_PING_COUNT] - - def ping(self): - """Send an ICMP echo request and return True if success.""" - next_id = run_callback_threadsafe( - self.hass.loop, async_get_next_ping_id, self.hass - ).result() - - return icmp_ping( - self.ip_address, count=PING_ATTEMPTS_COUNT, timeout=1, id=next_id - ).is_alive - - def update(self, see): - """Update device state by sending one or more ping messages.""" - if self.ping(): - see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER) - return True - - _LOGGER.debug( - "No response from %s (%s) failed=%d", - self.ip_address, - self.dev_id, - PING_ATTEMPTS_COUNT, - ) - - -def setup_scanner(hass, config, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the Host objects and return the update function.""" - try: - # Verify we can create a raw socket, or - # fallback to using a subprocess - icmp_ping("127.0.0.1", count=0, timeout=0) - host_cls = HostICMPLib - except SocketPermissionError: - host_cls = HostSubProcess - - hosts = [ - host_cls(ip, dev_id, hass, config) - for (dev_id, ip) in config[const.CONF_HOSTS].items() - ] + privileged = hass.data[DOMAIN][PING_PRIVS] + ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[const.CONF_HOSTS].items()} interval = config.get( CONF_SCAN_INTERVAL, - timedelta(seconds=len(hosts) * config[CONF_PING_COUNT]) + SCAN_INTERVAL, + timedelta(seconds=len(ip_to_dev_id) * config[CONF_PING_COUNT]) + SCAN_INTERVAL, ) _LOGGER.debug( "Started ping tracker with interval=%s on hosts: %s", interval, - ",".join([host.ip_address for host in hosts]), + ",".join(ip_to_dev_id.keys()), ) - def update_interval(now): - """Update all the hosts on every interval time.""" - try: - for host in hosts: - host.update(see) - finally: - hass.helpers.event.track_point_in_utc_time( - update_interval, util.dt.utcnow() + interval + if privileged is None: + hosts = [ + HostSubProcess(ip, dev_id, hass, config, privileged) + for (dev_id, ip) in config[const.CONF_HOSTS].items() + ] + + async def async_update(now): + """Update all the hosts on every interval time.""" + results = await gather_with_concurrency( + CONCURRENT_PING_LIMIT, + *[hass.async_add_executor_job(host.update) for host in hosts], + ) + await asyncio.gather( + *[ + async_see(dev_id=host.dev_id, source_type=SOURCE_TYPE_ROUTER) + for idx, host in enumerate(hosts) + if results[idx] + ] ) - update_interval(None) + else: + + async def async_update(now): + """Update all the hosts on every interval time.""" + responses = await hass.async_add_executor_job( + partial( + multiping, + ip_to_dev_id.keys(), + count=PING_ATTEMPTS_COUNT, + timeout=ICMP_TIMEOUT, + privileged=privileged, + id=async_get_next_ping_id(hass), + ) + ) + _LOGGER.debug("Multiping responses: %s", responses) + await asyncio.gather( + *[ + async_see(dev_id=dev_id, source_type=SOURCE_TYPE_ROUTER) + for idx, dev_id in enumerate(ip_to_dev_id.values()) + if responses[idx].is_alive + ] + ) + + async def _async_update_interval(now): + try: + await async_update(now) + finally: + 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 258a75caa02..09954787608 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,6 +3,6 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], - "requirements": ["icmplib==2.0"], + "requirements": ["icmplib==2.1.1"], "quality_scale": "internal" } diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 2cf97d5fd9a..2ec6028f9f9 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -37,7 +37,6 @@ from homeassistant.const import ( VOLUME_LITERS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -134,9 +133,7 @@ async def async_setup_coordinator(hass: HomeAssistant, entry: ConfigEntry): update_interval = timedelta(minutes=DEFAULT_SCAN_INTERVAL) coordinator = PlaatoCoordinator(hass, auth_token, device_type, update_interval) - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() _set_entry_data(entry, hass, coordinator, auth_token) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 8dbf6d50fca..2cb1f4ce326 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -17,12 +17,12 @@ from .const import ( CONF_USE_WEBHOOK, DEFAULT_SCAN_INTERVAL, DOCS_URL, + DOMAIN, PLACEHOLDER_DEVICE_NAME, PLACEHOLDER_DEVICE_TYPE, PLACEHOLDER_DOCS_URL, PLACEHOLDER_WEBHOOK_URL, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index 7cb1a77a9fb..a28dfefb567 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -68,7 +68,7 @@ class PlaatoEntity(entity.Entity): return device_info @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the monitored installation.""" if self._attributes: return { diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index ae767add18b..9af16a1cacd 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -1,10 +1,10 @@ """Support for Plaato Airlock sensors.""" -from typing import Optional +from __future__ import annotations from pyplaato.models.device import PlaatoDevice from pyplaato.plaato import PlaatoKeg -from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -59,15 +59,17 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class PlaatoSensor(PlaatoEntity): +class PlaatoSensor(PlaatoEntity, SensorEntity): """Representation of a Plaato Sensor.""" @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - if self._coordinator is not None: - if self._sensor_type == PlaatoKeg.Pins.TEMPERATURE: - return DEVICE_CLASS_TEMPERATURE + if ( + self._coordinator is not None + and self._sensor_type == PlaatoKeg.Pins.TEMPERATURE + ): + return DEVICE_CLASS_TEMPERATURE if self._sensor_type == ATTR_TEMP: return DEVICE_CLASS_TEMPERATURE return None diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index 852ecc88dde..85bc39a8d83 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -23,7 +23,7 @@ } }, "error": { - "invalid_webhook_device": "You have selected a device that doesn't not support sending data to a webhook. It is only available for the Airlock", + "invalid_webhook_device": "You have selected a device that does not support sending data to a webhook. It is only available for the Airlock", "no_auth_token": "You need to add an auth token", "no_api_method": "You need to add an auth token or select webhook" }, diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index eaf68b507f9..9a092ef4fa6 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -8,10 +8,43 @@ "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, + "error": { + "invalid_webhook_device": "Du hast ein Ger\u00e4t gew\u00e4hlt, das das Senden von Daten an einen Webhook nicht unterst\u00fctzt. Es ist nur f\u00fcr die Airlock verf\u00fcgbar", + "no_api_method": "Du musst ein Authentifizierungstoken hinzuf\u00fcgen oder ein Webhook ausw\u00e4hlen", + "no_auth_token": "Du musst ein Authentifizierungstoken hinzuf\u00fcgen" + }, + "step": { + "api_method": { + "data": { + "use_webhook": "Webhook verwenden" + }, + "title": "API-Methode ausw\u00e4hlen" + }, + "user": { + "data": { + "device_name": "Benenne dein Ger\u00e4t", + "device_type": "Art des Plaato-Ger\u00e4ts" + }, + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "title": "Plaato Webhook einrichten" + }, + "webhook": { + "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + } + } + }, + "options": { "step": { "user": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?", - "title": "Plaato Webhook einrichten" + "data": { + "update_interval": "Aktualisierungsintervall (Minuten)" + }, + "description": "Aktualisierungsintervall einrichten (Minuten)", + "title": "Optionen f\u00fcr Plaato" + }, + "webhook": { + "description": "Webhook-Informationen:\n\n- URL: `{webhook_url}`\n- Methode: POST\n\n", + "title": "Optionen f\u00fcr Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/en.json b/homeassistant/components/plaato/translations/en.json index 64d41d0091e..1217eb53d6e 100644 --- a/homeassistant/components/plaato/translations/en.json +++ b/homeassistant/components/plaato/translations/en.json @@ -9,7 +9,7 @@ "default": "Your Plaato {device_type} with name **{device_name}** was successfully setup!" }, "error": { - "invalid_webhook_device": "You have selected a device that doesn't not support sending data to a webhook. It is only available for the Airlock", + "invalid_webhook_device": "You have selected a device that does not support sending data to a webhook. It is only available for the Airlock", "no_api_method": "You need to add an auth token or select webhook", "no_auth_token": "You need to add an auth token" }, diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json index 66a7b252a87..fb4325581dd 100644 --- a/homeassistant/components/plaato/translations/et.json +++ b/homeassistant/components/plaato/translations/et.json @@ -9,7 +9,7 @@ "default": "{device_type} Plaato seade nimega **{device_name}** on edukalt seadistatud!" }, "error": { - "invalid_webhook_device": "Oled valinud seadme, mis ei toeta andmete saatmist veebihaagile. See on saadaval ainult Airlocki jaoks", + "invalid_webhook_device": "Oled valinud seadme mis ei toeta andmete saatmist veebihaagile. See on saadaval ainult Airlocki jaoks", "no_api_method": "Pead lisama autentimisloa v\u00f5i valima veebihaagi", "no_auth_token": "Pead lisama autentimisloa" }, diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 76229e86224..8347b5d2f98 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + "default": "A Plaato {device_type} **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" + }, + "step": { + "user": { + "data": { + "device_name": "Eszk\u00f6z neve", + "device_type": "A Plaato eszk\u00f6z t\u00edpusa" + }, + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "title": "A Plaato eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" + } } } } \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/id.json b/homeassistant/components/plaato/translations/id.json new file mode 100644 index 00000000000..989bb38bcaf --- /dev/null +++ b/homeassistant/components/plaato/translations/id.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Plaato {device_type} dengan nama **{device_name}** berhasil disiapkan!" + }, + "error": { + "invalid_webhook_device": "Anda telah memilih perangkat yang tidak mendukung pengiriman data ke webhook. Ini hanya tersedia untuk Airlock", + "no_api_method": "Anda perlu menambahkan token auth atau memilih webhook", + "no_auth_token": "Anda perlu menambahkan token autentikasi" + }, + "step": { + "api_method": { + "data": { + "token": "Tempel Token Auth di sini", + "use_webhook": "Gunakan webhook" + }, + "description": "Untuk dapat melakukan kueri API diperlukan 'auth_token'. Nilai token dapat diperoleh dengan mengikuti [petunjuk ini](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\nPerangkat yang dipilih: **{device_type}** \n\nJika Anda lebih memilih untuk menggunakan metode webhook bawaan (hanya Airlock), centang centang kotak di bawah ini dan kosongkan nilai Auth Token'", + "title": "Pilih metode API" + }, + "user": { + "data": { + "device_name": "Nama perangkat Anda", + "device_type": "Jenis perangkat Plaato" + }, + "description": "Ingin memulai penyiapan?", + "title": "Siapkan perangkat Plaato" + }, + "webhook": { + "description": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di Plaato Airlock.\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nBaca [dokumentasi]({docs_url}) tentang detail lebih lanjut.", + "title": "Webhook untuk digunakan" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Interval pembaruan (menit)" + }, + "description": "Atur interval pembaruan (menit)", + "title": "Opsi untuk Plaato" + }, + "webhook": { + "description": "Info webhook:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n", + "title": "Opsi untuk Plaato Airlock" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ko.json b/homeassistant/components/plaato/translations/ko.json index 753653c88b2..fb75bbb7d7c 100644 --- a/homeassistant/components/plaato/translations/ko.json +++ b/homeassistant/components/plaato/translations/ko.json @@ -2,16 +2,52 @@ "config": { "abort": { "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "**{device_name}** \uc758 Plaato {device_type} \uc774(\uac00) \uc131\uacf5\uc801\uc73c\ub85c \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4!" + "default": "**{device_name}**\uc758 Plaato {device_type}\uc774(\uac00) \uc131\uacf5\uc801\uc73c\ub85c \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4!" + }, + "error": { + "invalid_webhook_device": "\uc6f9 \ud6c5\uc73c\ub85c \ub370\uc774\ud130 \uc804\uc1a1\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud588\uc2b5\ub2c8\ub2e4. Airlock\uc5d0\uc11c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4", + "no_api_method": "\uc778\uc99d \ud1a0\ud070\uc744 \ucd94\uac00\ud558\uac70\ub098 \uc6f9 \ud6c5\uc744 \uc120\ud0dd\ud574\uc57c \ud569\ub2c8\ub2e4", + "no_auth_token": "\uc778\uc99d \ud1a0\ud070\uc744 \ucd94\uac00\ud574\uc57c \ud569\ub2c8\ub2e4" }, "step": { + "api_method": { + "data": { + "token": "\uc5ec\uae30\uc5d0 \uc778\uc99d \ud1a0\ud070\uc744 \ubd99\uc5ec \ub123\uc5b4\uc8fc\uc138\uc694", + "use_webhook": "\uc6f9 \ud6c5 \uc0ac\uc6a9\ud558\uae30" + }, + "description": "API\ub97c \ucffc\ub9ac \ud558\ub824\uba74 [\uc548\ub0b4 \uc9c0\uce68](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\uc5d0 \ub530\ub77c \uc5bb\uc744 \uc218 \uc788\ub294 'auth_token'\uc774 \ud544\uc694\ud569\ub2c8\ub2e4\n\n\uc120\ud0dd\ud55c \uae30\uae30: **{device_type}**\n\n\ub0b4\uc7a5\ub41c \uc6f9 \ud6c5 \ubc29\uc2dd(Airlock \uc804\uc6a9)\uc744 \uc0ac\uc6a9\ud558\ub824\ub294 \uacbd\uc6b0 \uc544\ub798 \ud655\uc778\ub780\uc744 \uc120\ud0dd\ud558\uace0 Auth Token\uc744 \ube44\uc6cc\ub450\uc138\uc694", + "title": "API \ubc29\uc2dd \uc120\ud0dd\ud558\uae30" + }, "user": { + "data": { + "device_name": "\uae30\uae30 \uc774\ub984", + "device_type": "Plaato \uae30\uae30 \uc720\ud615" + }, "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Plaato \uae30\uae30 \uc124\uc815\ud558\uae30" + }, + "webhook": { + "description": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Plaato Airlock\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "\uc0ac\uc6a9\ud560 \uc6f9 \ud6c5" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ubd84)" + }, + "description": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 \uc124\uc815\ud558\uae30 (\ubd84)", + "title": "Plaato \uc635\uc158" + }, + "webhook": { + "description": "\uc6f9 \ud6c5 \uc815\ubcf4:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n", + "title": "Plaato Airlock \uc635\uc158" } } } diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index c0a9d1e04fb..d50763d0e1a 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -6,9 +6,11 @@ "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." }, "create_entry": { - "default": "Om evenementen naar de Home Assistant te sturen, moet u de webhook-functie instellen in Plaato Airlock. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ( {docs_url} ) voor meer informatie." + "default": "Uw Plaato {device_type} met naam **{device_name}** is succesvol ingesteld!" }, "error": { + "invalid_webhook_device": "U heeft een apparaat geselecteerd dat het verzenden van gegevens naar een webhook niet ondersteunt. Het is alleen beschikbaar voor de Airlock", + "no_api_method": "U moet een verificatie token toevoegen of selecteer een webhook", "no_auth_token": "U moet een verificatie token toevoegen" }, "step": { @@ -17,6 +19,7 @@ "token": "Plak hier de verificatie-token", "use_webhook": "Webhook gebruiken" }, + "description": "Om de API te kunnenopvragen is een `auth_token` nodig, die kan worden verkregen door [deze] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructies te volgen\n\n Geselecteerd apparaat: **{device_type}** \n\nIndien u liever de ingebouwde webhook methode gebruikt (alleen Airlock) vink dan het vakje hieronder aan en laat Auth Token leeg", "title": "Selecteer API-methode" }, "user": { @@ -24,17 +27,26 @@ "device_name": "Geef uw apparaat een naam", "device_type": "Type Plaato-apparaat" }, - "description": "Weet u zeker dat u de Plaato-airlock wilt instellen?", - "title": "Stel de Plaato Webhook in" + "description": "Wil je beginnen met instellen?", + "title": "Stel de Plaato-apparaten in" + }, + "webhook": { + "description": "Om evenementen naar de Home Assistant te sturen, moet u de webhook-functie instellen in Plaato Airlock. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ({docs_url}) voor meer informatie.", + "title": "Webhook om te gebruiken" } } }, "options": { "step": { "user": { + "data": { + "update_interval": "Update-interval (minuten)" + }, + "description": "Stel het update-interval in (minuten)", "title": "Opties voor Plaato" }, "webhook": { + "description": "Webhook-informatie: \n\n - URL: ' {webhook_url} '\n - Methode: POST\n\n", "title": "Opties voor Plaato Airlock" } } diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json index 8efbf07945f..7039662468e 100644 --- a/homeassistant/components/plaato/translations/no.json +++ b/homeassistant/components/plaato/translations/no.json @@ -9,7 +9,7 @@ "default": "Plaato {device_type} med navnet **{device_name}** ble konfigurert!" }, "error": { - "invalid_webhook_device": "Du har valgt en enhet som ikke st\u00f8tter sending av data til en webhook. Den er kun tilgjengelig for Airlock", + "invalid_webhook_device": "Du har valgt en enhet som ikke st\u00f8tter sending av data til en webhook. Den er bare tilgjengelig for luftsluse", "no_api_method": "Du m\u00e5 legge til et godkjenningstoken eller velge webhook", "no_auth_token": "Du m\u00e5 legge til et godkjenningstoken" }, diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index 57df32c3f4e..ddfb779ea2e 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Tw\u00f3j {device_type} Plaato o nazwie *{device_name}* zosta\u0142o pomy\u015blnie skonfigurowane!" + "default": "Tw\u00f3j {device_type} Plaato o nazwie **{device_name}** zosta\u0142o pomy\u015blnie skonfigurowane!" }, "error": { "invalid_webhook_device": "Wybra\u0142e\u015b urz\u0105dzenie, kt\u00f3re nie obs\u0142uguje wysy\u0142ania danych do webhooka. Opcja dost\u0119pna tylko w areometrze Airlock", @@ -19,7 +19,7 @@ "token": "Wklej token autoryzacji", "use_webhook": "U\u017cyj webhook" }, - "description": "Aby m\u00f3c przesy\u0142a\u0107 zapytania do API, wymagany jest \u201etoken autoryzacji\u201d, kt\u00f3ry mo\u017cna uzyska\u0107, post\u0119puj\u0105c zgodnie z [t\u0105] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrukcj\u0105\n\nWybrane urz\u0105dzenie: *{device_type}* \n\nJe\u015bli wolisz u\u017cywa\u0107 wbudowanej metody webhook (tylko areomierz Airlock), zaznacz poni\u017csze pole i pozostaw token autoryzacji pusty", + "description": "Aby m\u00f3c przesy\u0142a\u0107 zapytania do API, wymagany jest \u201etoken autoryzacji\u201d, kt\u00f3ry mo\u017cna uzyska\u0107, post\u0119puj\u0105c zgodnie z [t\u0105] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrukcj\u0105\n\nWybrane urz\u0105dzenie: **{device_type}** \n\nJe\u015bli wolisz u\u017cywa\u0107 wbudowanej metody webhook (tylko areomierz Airlock), zaznacz poni\u017csze pole i pozostaw token autoryzacji pusty", "title": "Wybierz metod\u0119 API" }, "user": { diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 2d829187560..290993959b3 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -1,5 +1,6 @@ """Support for monitoring plants.""" from collections import deque +from contextlib import suppress from datetime import datetime, timedelta import logging @@ -324,12 +325,10 @@ class Plant(Entity): for state in states: # filter out all None, NaN and "unknown" states # only keep real values - try: + with suppress(ValueError): self._brightness_history.add_measurement( int(state.state), state.last_updated ) - except ValueError: - pass _LOGGER.debug("Initializing from database completed") @property @@ -348,7 +347,7 @@ class Plant(Entity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the entity. Provide the individual measurements from the diff --git a/homeassistant/components/plant/translations/id.json b/homeassistant/components/plant/translations/id.json index 519964be278..5378edc405f 100644 --- a/homeassistant/components/plant/translations/id.json +++ b/homeassistant/components/plant/translations/id.json @@ -1,9 +1,9 @@ { "state": { "_": { - "ok": "OK", - "problem": "Masalah" + "ok": "Oke", + "problem": "Bermasalah" } }, - "title": "Tanaman" + "title": "Monitor Tanaman" } \ No newline at end of file diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 6b403150e9c..137c0524bac 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -7,7 +7,6 @@ import plexapi.exceptions from plexapi.gdm import GDM from plexwebsocket import ( SIGNAL_CONNECTION_STATE, - SIGNAL_DATA, STATE_CONNECTED, STATE_DISCONNECTED, STATE_STOPPED, @@ -25,9 +24,13 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import 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 -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( CONF_SERVER, @@ -39,6 +42,7 @@ from .const import ( PLATFORMS, PLATFORMS_COMPLETED, PLEX_SERVER_CONFIG, + PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, WEBSOCKETS, @@ -61,12 +65,16 @@ async def async_setup(hass, config): gdm = hass.data[PLEX_DOMAIN][GDM_SCANNER] = GDM() + def gdm_scan(): + _LOGGER.debug("Scanning for GDM clients") + gdm.scan(scan_for_clients=True) + hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer( hass, _LOGGER, cooldown=10, immediate=True, - function=partial(gdm.scan, scan_for_clients=True), + function=gdm_scan, ).async_call return True @@ -145,26 +153,22 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) - async def async_update_plex(): - await hass.data[PLEX_DOMAIN][GDM_DEBOUNCER]() - await plex_server.async_update_platforms() - unsub = async_dispatcher_connect( hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), - async_update_plex, + plex_server.async_update_platforms, ) hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) @callback - def plex_websocket_callback(signal, data, error): + def plex_websocket_callback(msgtype, data, error): """Handle callbacks from plexwebsocket library.""" - if signal == SIGNAL_CONNECTION_STATE: + if msgtype == SIGNAL_CONNECTION_STATE: if data == STATE_CONNECTED: _LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER]) - hass.async_create_task(async_update_plex()) + hass.async_create_task(plex_server.async_update_platforms()) elif data == STATE_DISCONNECTED: _LOGGER.debug( "Websocket to %s disconnected, retrying", entry.data[CONF_SERVER] @@ -178,14 +182,22 @@ async def async_setup_entry(hass, entry): ) hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) - elif signal == SIGNAL_DATA: + elif msgtype == "playing": hass.async_create_task(plex_server.async_update_session(data)) + elif msgtype == "status": + if data["StatusNotification"][0]["title"] == "Library scan complete": + async_dispatcher_send( + hass, + PLEX_UPDATE_LIBRARY_SIGNAL.format(server_id), + ) session = async_get_clientsession(hass) + subscriptions = ["playing", "status"] verify_ssl = server_config.get(CONF_VERIFY_SSL) websocket = PlexWebsocket( plex_server.plex_server, plex_websocket_callback, + subscriptions=subscriptions, session=session, verify_ssl=verify_ssl, ) @@ -210,6 +222,8 @@ async def async_setup_entry(hass, entry): ) task.add_done_callback(partial(start_websocket_session, platform)) + async_cleanup_plex_devices(hass, entry) + def get_plex_account(plex_server): try: return plex_server.account @@ -250,3 +264,30 @@ async def async_options_updated(hass, entry): # Guard incomplete setup during reauth flows if server_id in hass.data[PLEX_DOMAIN][SERVERS]: hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options + + +@callback +def async_cleanup_plex_devices(hass, entry): + """Clean up old and invalid devices from the registry.""" + device_registry = dev_reg.async_get(hass) + entity_registry = ent_reg.async_get(hass) + + device_entries = hass.helpers.device_registry.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + + for device_entry in device_entries: + if ( + len( + hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_entry.id, include_disabled_entities=True + ) + ) + == 0 + ): + _LOGGER.debug( + "Removing orphaned device: %s / %s", + device_entry.name, + device_entry.identifiers, + ) + device_registry.async_remove_device(device_entry.id) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index d611c09c43e..d1fa5684cf5 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -27,7 +27,7 @@ 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 ( # pylint: disable=unused-import +from .const import ( AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, AUTOMATIC_SETUP_STRING, diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index eec433202e4..e247f7a5db7 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -4,6 +4,7 @@ from homeassistant.const import __version__ DOMAIN = "plex" NAME_FORMAT = "Plex ({})" COMMON_PLAYERS = ["Plex Web"] +TRANSIENT_DEVICE_MODELS = ["Plex Web", "Plex for Sonos"] DEFAULT_PORT = 32400 DEFAULT_SSL = False @@ -26,6 +27,7 @@ PLEX_SERVER_CONFIG = "server_config" PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL = "plex_update_session_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_LIBRARY_SIGNAL = "plex_update_libraries_signal.{}" PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 1319e4bbf49..e0e62d7150b 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,9 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.4.1", + "plexapi==4.5.1", "plexauth==0.0.6", - "plexwebsocket==0.0.12" + "plexwebsocket==0.0.13" ], "dependencies": ["http"], "codeowners": ["@jjlawren"] diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index cfc5a12d6c5..f3f92880c44 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -153,7 +153,7 @@ def browse_media(entity, is_internal, media_content_type=None, media_content_id= title = entity.plex_server.friendly_name elif media_content_type == "library": library_or_section = entity.plex_server.library.sectionByID( - media_content_id + int(media_content_id) ) title = library_or_section.title try: @@ -193,7 +193,7 @@ def browse_media(entity, is_internal, media_content_type=None, media_content_id= return server_payload(entity.plex_server) if media_content_type == "library": - return library_payload(media_content_id) + return library_payload(int(media_content_id)) except UnknownMediaType as err: raise BrowseError( @@ -223,7 +223,7 @@ def library_section_payload(section): return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=section.key, + media_content_id=str(section.key), media_content_type="library", can_play=False, can_expand=True, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1a57186bd9b..650ed2c89b0 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -41,6 +41,7 @@ from .const import ( PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, + TRANSIENT_DEVICE_MODELS, ) from .media_browser import browse_media @@ -522,7 +523,7 @@ class PlexMediaPlayer(MediaPlayerEntity): _LOGGER.error("Timed out playing on %s", self.name) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the scene state attributes.""" attributes = {} for attr in [ @@ -544,6 +545,15 @@ class PlexMediaPlayer(MediaPlayerEntity): if self.machine_identifier is None: return None + if self.device_product in TRANSIENT_DEVICE_MODELS: + return { + "identifiers": {(PLEX_DOMAIN, "plex.tv-clients")}, + "name": "Plex Client Service", + "manufacturer": "Plex", + "model": "Plex Clients", + "entry_type": "service", + } + return { "identifiers": {(PLEX_DOMAIN, self.machine_identifier)}, "manufacturer": self.device_platform or "Plex", diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 8c3733a7450..95ba0a65ef0 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,19 +1,39 @@ """Support for Plex media server monitoring.""" import logging +from plexapi.exceptions import NotFound + +from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import ( CONF_SERVER_IDENTIFIER, - DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, + PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) +LIBRARY_ATTRIBUTE_TYPES = { + "artist": ["artist", "album"], + "photo": ["photoalbum"], + "show": ["show", "season"], +} + +LIBRARY_PRIMARY_LIBTYPE = { + "show": "episode", + "artist": "track", +} + +LIBRARY_ICON_LOOKUP = { + "artist": "mdi:music", + "movie": "mdi:movie", + "photo": "mdi:image", + "show": "mdi:television", +} + _LOGGER = logging.getLogger(__name__) @@ -21,11 +41,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] - sensor = PlexSensor(hass, plexserver) - async_add_entities([sensor]) + sensors = [PlexSensor(hass, plexserver)] + + def create_library_sensors(): + """Create Plex library sensors with sync calls.""" + for library in plexserver.library.sections(): + sensors.append(PlexLibrarySectionSensor(hass, plexserver, library)) + + await hass.async_add_executor_job(create_library_sensors) + async_add_entities(sensors) -class PlexSensor(Entity): +class PlexSensor(SensorEntity): """Representation of a Plex now playing sensor.""" def __init__(self, hass, plex_server): @@ -45,12 +72,13 @@ class PlexSensor(Entity): async def async_added_to_hass(self): """Run when about to be added to hass.""" server_id = self._server.machine_identifier - unsub = async_dispatcher_connect( - self.hass, - PLEX_UPDATE_SENSOR_SIGNAL.format(server_id), - self.async_refresh_sensor, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(server_id), + self.async_refresh_sensor, + ) ) - self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) async def _async_refresh_sensor(self): """Set instance object and trigger an entity state update.""" @@ -89,7 +117,7 @@ class PlexSensor(Entity): return "mdi:plex" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._server.sensor_attributes @@ -103,6 +131,117 @@ class PlexSensor(Entity): "identifiers": {(PLEX_DOMAIN, self._server.machine_identifier)}, "manufacturer": "Plex", "model": "Plex Media Server", - "name": "Activity Sensor", + "name": self._server.friendly_name, + "sw_version": self._server.version, + } + + +class PlexLibrarySectionSensor(SensorEntity): + """Representation of a Plex library section sensor.""" + + def __init__(self, hass, plex_server, plex_library_section): + """Initialize the sensor.""" + self._server = plex_server + self.server_name = plex_server.friendly_name + self.server_id = plex_server.machine_identifier + self.library_section = plex_library_section + self.library_type = plex_library_section.type + self._name = f"{self.server_name} Library - {plex_library_section.title}" + self._unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" + self._state = None + self._available = True + self._attributes = {} + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + PLEX_UPDATE_LIBRARY_SIGNAL.format(self.server_id), + self.async_refresh_sensor, + ) + ) + await self.async_refresh_sensor() + + async def async_refresh_sensor(self): + """Update state and attributes for the library sensor.""" + _LOGGER.debug("Refreshing library sensor for '%s'", self.name) + try: + await self.hass.async_add_executor_job(self._update_state_and_attrs) + self._available = True + except NotFound: + self._available = False + self.async_write_ha_state() + + def _update_state_and_attrs(self): + """Update library sensor state with sync calls.""" + primary_libtype = LIBRARY_PRIMARY_LIBTYPE.get( + self.library_type, self.library_type + ) + + self._state = self.library_section.totalViewSize( + libtype=primary_libtype, includeCollections=False + ) + for libtype in LIBRARY_ATTRIBUTE_TYPES.get(self.library_type, []): + self._attributes[f"{libtype}s"] = self.library_section.totalViewSize( + libtype=libtype, includeCollections=False + ) + + @property + def available(self): + """Return the availability of the client.""" + return self._available + + @property + def entity_registry_enabled_default(self): + """Return if sensor should be enabled by default.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unique_id(self): + """Return the id of this plex client.""" + return self._unique_id + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return "Items" + + @property + def icon(self): + """Return the icon of the sensor.""" + return LIBRARY_ICON_LOOKUP.get(self.library_type, "mdi:plex") + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def device_info(self): + """Return a device description for device registry.""" + if self.unique_id is None: + return None + + return { + "identifiers": {(PLEX_DOMAIN, self.server_id)}, + "manufacturer": "Plex", + "model": "Plex Media Server", + "name": self.server_name, "sw_version": self._server.version, } diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 8f9d4d1cc51..d4bd4b09ef2 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -34,6 +34,7 @@ from .const import ( DEBOUNCE_TIMEOUT, DEFAULT_VERIFY_SSL, DOMAIN, + GDM_DEBOUNCER, GDM_SCANNER, PLAYER_SOURCE, PLEX_NEW_MP_SIGNAL, @@ -189,7 +190,7 @@ class PlexServer: _connect_with_url() except requests.exceptions.SSLError as error: while error and not isinstance(error, ssl.SSLCertVerificationError): - error = error.__context__ # pylint: disable=no-member + error = error.__context__ if isinstance(error, ssl.SSLCertVerificationError): domain = urlparse(self._url).netloc.split(":")[0] if domain.endswith("plex.direct") and error.args[0].startswith( @@ -256,11 +257,7 @@ class PlexServer: async def async_update_session(self, payload): """Process a session payload received from a websocket callback.""" - try: - session_payload = payload["PlaySessionStateNotification"][0] - except KeyError: - await self.async_update_platforms() - return + session_payload = payload["PlaySessionStateNotification"][0] state = session_payload["state"] if state == "buffering": @@ -323,6 +320,8 @@ class PlexServer: """Update the platform entities.""" _LOGGER.debug("Updating devices") + await self.hass.data[DOMAIN][GDM_DEBOUNCER]() + available_clients = {} ignored_clients = set() new_clients = set() @@ -366,17 +365,20 @@ class PlexServer: PLAYER_SOURCE, source ) - if device.machineIdentifier not in ignored_clients: - if self.option_ignore_plexweb_clients and device.product == "Plex Web": - ignored_clients.add(device.machineIdentifier) - if device.machineIdentifier not in self._known_clients: - _LOGGER.debug( - "Ignoring %s %s: %s", - "Plex Web", - source, - device.machineIdentifier, - ) - return + if ( + device.machineIdentifier not in ignored_clients + and self.option_ignore_plexweb_clients + and device.product == "Plex Web" + ): + ignored_clients.add(device.machineIdentifier) + if device.machineIdentifier not in self._known_clients: + _LOGGER.debug( + "Ignoring %s %s: %s", + "Plex Web", + source, + device.machineIdentifier, + ) + return if device.machineIdentifier not in ( self._created_clients | ignored_clients | new_clients @@ -395,9 +397,10 @@ class PlexServer: client = PlexClient( server=self._plex_server, baseurl=baseurl, + identifier=machine_identifier, token=self._plex_server.createToken(), ) - except requests.exceptions.ConnectionError: + except (NotFound, requests.exceptions.ConnectionError): _LOGGER.error( "Direct client connection failed, will try again: %s (%s)", name, @@ -418,9 +421,11 @@ class PlexServer: """Connect to a plex.tv resource and return a Plex client.""" try: client = resource.connect(timeout=3) - _LOGGER.debug("plex.tv resource connection successful: %s", client) + _LOGGER.debug("Resource connection successful to plex.tv: %s", client) except NotFound: - _LOGGER.error("plex.tv resource connection failed: %s", resource.name) + _LOGGER.error( + "Resource connection failed to plex.tv: %s", resource.name + ) else: client.proxyThroughServer(value=False, server=self._plex_server) self._client_device_cache[client.machineIdentifier] = client diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index cfccf5c83e6..9168f070609 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -3,26 +3,30 @@ "abort": { "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", - "unknown": "Ismeretlen okb\u00f3l nem siker\u00fclt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen", "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz", "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { "host": "Hoszt", "port": "Port", - "ssl": "Haszn\u00e1ljon SSL-t" + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "token": "Token (opcion\u00e1lis)", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } }, "select_server": { "data": { - "server": "szerver" + "server": "Szerver" }, "description": "T\u00f6bb szerver el\u00e9rhet\u0151, v\u00e1lasszon egyet:", "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa" @@ -39,6 +43,7 @@ "step": { "plex_mp_settings": { "data": { + "ignore_plex_web_clients": "Plex Web kliensek figyelmen k\u00edv\u00fcl hagy\u00e1sa", "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t" }, "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai" diff --git a/homeassistant/components/plex/translations/id.json b/homeassistant/components/plex/translations/id.json new file mode 100644 index 00000000000..7c596835e03 --- /dev/null +++ b/homeassistant/components/plex/translations/id.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "Semua server tertaut sudah dikonfigurasi", + "already_configured": "Server Plex ini sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "reauth_successful": "Autentikasi ulang berhasil", + "token_request_timeout": "Tenggang waktu pengambilan token habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "faulty_credentials": "Otorisasi gagal, verifikasi Token", + "host_or_token": "Harus menyediakan setidaknya satu Host atau Token", + "no_servers": "Tidak ada server yang ditautkan ke akun Plex", + "not_found": "Server Plex tidak ditemukan", + "ssl_error": "Masalah sertifikat SSL" + }, + "flow_title": "{name} ({host})", + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "token": "Token (Opsional)", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "title": "Konfigurasi Plex Manual" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Beberapa server tersedia, pilih satu:", + "title": "Pilih server Plex" + }, + "user": { + "description": "Lanjutkan [plex.tv](https://plex.tv) untuk menautkan server Plex.", + "title": "Server Media Plex" + }, + "user_advanced": { + "data": { + "setup_method": "Metode penyiapan" + }, + "title": "Server Media Plex" + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "Abaikan pengguna baru yang dikelola/berbagi", + "ignore_plex_web_clients": "Abaikan klien Plex Web", + "monitored_users": "Pengguna yang dipantau", + "use_episode_art": "Gunakan sampul episode" + }, + "description": "Opsi untuk Pemutar Media Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json index df728533467..d6b0c6a2341 100644 --- a/homeassistant/components/plex/translations/ko.json +++ b/homeassistant/components/plex/translations/ko.json @@ -35,7 +35,7 @@ "title": "Plex \uc11c\ubc84 \uc120\ud0dd\ud558\uae30" }, "user": { - "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv) \ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", + "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv)\ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", "title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84" }, "user_advanced": { diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index 6c89b0b8d5f..a196555e7ea 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -3,15 +3,15 @@ "abort": { "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", "already_configured": "Deze Plex-server is al geconfigureerd", - "already_in_progress": "Plex wordt geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "reauth_successful": "Herauthenticatie was succesvol", "token_request_timeout": "Time-out verkrijgen van token", - "unknown": "Mislukt om onbekende reden" + "unknown": "Onverwachte fout" }, "error": { - "faulty_credentials": "Autorisatie mislukt", + "faulty_credentials": "Autorisatie mislukt, controleer token", "host_or_token": "Moet ten minste \u00e9\u00e9n host of token verstrekken.", - "no_servers": "Geen servers gekoppeld aan account", + "no_servers": "Geen servers gekoppeld aan Plex account", "not_found": "Plex-server niet gevonden", "ssl_error": "SSL-certificaatprobleem" }, diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 825d27d59bb..023ffa3de70 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -143,7 +143,7 @@ class PwNotifySensor(SmileBinarySensor, BinarySensorEntity): self._attributes = {} @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index c8a2191963e..3efd6dbc3ca 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -124,7 +124,7 @@ class PwThermostat(SmileGateway, ClimateEntity): return SUPPORT_FLAGS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attributes = {} if self._schema_names: diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index c14395319d4..70a4a822431 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -1,9 +1,9 @@ """Plugwise platform for Home Assistant Core.""" +from __future__ import annotations import asyncio from datetime import timedelta import logging -from typing import Dict import async_timeout from plugwise.exceptions import ( @@ -103,16 +103,12 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=update_interval, ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() api.get_all_devices() - if entry.unique_id is None: - if api.smile_version[0] != "1.8.0": - hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname) + if entry.unique_id is None and api.smile_version[0] != "1.8.0": + hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname) undo_listener = entry.add_update_listener(_update_listener) @@ -139,9 +135,9 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: if single_master_thermostat is None: platforms = SENSOR_PLATFORMS - for component in platforms: + for platform in platforms: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -160,8 +156,8 @@ async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS_GATEWAY + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS_GATEWAY ] ) ) @@ -201,7 +197,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/sensor.py b/homeassistant/components/plugwise/sensor.py index f57ff2b2a91..4152f9fdabd 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -2,6 +2,7 @@ import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, @@ -17,7 +18,6 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from .const import ( COOL_ICON, @@ -236,7 +236,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class SmileSensor(SmileGateway): +class SmileSensor(SmileGateway, SensorEntity): """Represent Smile Sensors.""" def __init__(self, api, coordinator, name, dev_id, sensor): @@ -282,7 +282,7 @@ class SmileSensor(SmileGateway): return self._unit_of_measurement -class PwThermostatSensor(SmileSensor, Entity): +class PwThermostatSensor(SmileSensor): """Thermostat (or generic) sensor devices.""" def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type): @@ -311,7 +311,7 @@ class PwThermostatSensor(SmileSensor, Entity): self.async_write_ha_state() -class PwAuxDeviceSensor(SmileSensor, Entity): +class PwAuxDeviceSensor(SmileSensor): """Auxiliary Device Sensors.""" def __init__(self, api, coordinator, name, dev_id, sensor): @@ -348,7 +348,7 @@ class PwAuxDeviceSensor(SmileSensor, Entity): self.async_write_ha_state() -class PwPowerSensor(SmileSensor, Entity): +class PwPowerSensor(SmileSensor): """Power sensor entities.""" def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type, model): diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 1dcdb7fe5af..d6d9012c21a 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -1,13 +1,31 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s 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": "Smile: {name}", "step": { "user": { "data": { "flow_type": "Kapcsolat t\u00edpusa" - } + }, + "description": "Term\u00e9k:", + "title": "Plugwise t\u00edpus" }, "user_gateway": { - "description": "K\u00e9rj\u00fck, adja meg" + "data": { + "host": "IP c\u00edm", + "password": "Smile azonos\u00edt\u00f3", + "port": "Port", + "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rj\u00fck, adja meg", + "title": "Csatlakoz\u00e1s a Smile-hoz" } } } diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json new file mode 100644 index 00000000000..9047bf477bd --- /dev/null +++ b/homeassistant/components/plugwise/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Smile: {name}", + "step": { + "user": { + "data": { + "flow_type": "Jenis koneksi" + }, + "description": "Produk:", + "title": "Jenis Plugwise" + }, + "user_gateway": { + "data": { + "host": "Alamat IP", + "password": "ID Smile", + "port": "Port", + "username": "Nama Pengguna Smile" + }, + "description": "Masukkan", + "title": "Hubungkan ke Smile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval Pindai (detik)" + }, + "description": "Sesuaikan Opsi Plugwise" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json index 7af503f0a66..7d856d1fe28 100644 --- a/homeassistant/components/plugwise/translations/ko.json +++ b/homeassistant/components/plugwise/translations/ko.json @@ -11,14 +11,21 @@ "flow_title": "Smile: {name}", "step": { "user": { + "data": { + "flow_type": "\uc5f0\uacb0 \uc720\ud615" + }, "description": "\uc81c\ud488:", "title": "Plugwise \uc720\ud615" }, "user_gateway": { "data": { "host": "IP \uc8fc\uc18c", - "port": "\ud3ec\ud2b8" - } + "password": "Smile ID", + "port": "\ud3ec\ud2b8", + "username": "Smile \uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Smile\uc5d0 \uc5f0\uacb0\ud558\uae30" } } }, @@ -28,7 +35,7 @@ "data": { "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" }, - "description": "Plugwise \uc635\uc158 \uc870\uc815" + "description": "Plugwise \uc635\uc158 \uc870\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 001bbbe6d5e..af77f6f15e1 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -4,24 +4,27 @@ "already_configured": "De service is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", - "invalid_auth": "Ongeldige authenticatie, controleer de 8 karakters van uw Smile-ID", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, + "flow_title": "Glimlach: {name}", "step": { "user": { "data": { "flow_type": "Verbindingstype" }, - "description": "Details", - "title": "Maak verbinding met de Smile" + "description": "Product:", + "title": "Plugwise type" }, "user_gateway": { "data": { "host": "IP-adres", "password": "Smile-ID", - "port": "Poort" + "port": "Poort", + "username": "Smile Gebruikersnaam" }, + "description": "Voer in", "title": "Maak verbinding met de Smile" } } @@ -31,7 +34,8 @@ "init": { "data": { "scan_interval": "Scaninterval (seconden)" - } + }, + "description": "Plugwise opties aanpassen" } } } diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 9df460e8919..f027ebfc772 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -22,7 +22,7 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441", "password": "Smile ID", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d Smile" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" }, "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435:", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile" diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 2a7ce4497bb..aeabe8634f8 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: dict): conf = config[DOMAIN] - _LOGGER.info("Found Plum Lightpad configuration in config, importing...") + _LOGGER.info("Found Plum Lightpad configuration in config, importing") hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=conf @@ -67,9 +67,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = plum - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) def cleanup(event): diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index c6261307d13..40432810cc5 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Plum Lightpad.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from aiohttp import ContentTypeError from requests.exceptions import ConnectTimeout, HTTPError @@ -10,7 +12,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN from .utils import load_plum _LOGGER = logging.getLogger(__name__) @@ -34,8 +36,8 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() @@ -58,7 +60,7 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_import( - self, import_config: Optional[ConfigType] - ) -> Dict[str, Any]: + self, import_config: ConfigType | None + ) -> dict[str, Any]: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index feacec4492b..90558eb2523 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -1,6 +1,8 @@ """Support for Plum Lightpad lights.""" +from __future__ import annotations + import asyncio -from typing import Callable, List +from typing import Callable from plumlightpad import Plum @@ -23,7 +25,7 @@ from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity]], None], + async_add_entities: Callable[[list[Entity]], None], ) -> None: """Set up Plum Lightpad dimmer lights and glow rings.""" diff --git a/homeassistant/components/plum_lightpad/translations/hu.json b/homeassistant/components/plum_lightpad/translations/hu.json index 436e8b1fb7d..3a82e0a241e 100644 --- a/homeassistant/components/plum_lightpad/translations/hu.json +++ b/homeassistant/components/plum_lightpad/translations/hu.json @@ -2,6 +2,17 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/id.json b/homeassistant/components/plum_lightpad/translations/id.json new file mode 100644 index 00000000000..0d2f98d1faa --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 19f7e265438..55ae4a524fc 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -5,10 +5,9 @@ import logging from pycketcasts import pocketcasts import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -37,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return False -class PocketCastsSensor(Entity): +class PocketCastsSensor(SensorEntity): """Representation of a pocket casts sensor.""" def __init__(self, api): diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 13dce13c0da..e5c209004de 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -39,6 +39,8 @@ _LOGGER = logging.getLogger(__name__) DATA_CONFIG_ENTRY_LOCK = "point_config_entry_lock" CONFIG_ENTRY_IS_SETUP = "point_config_entry_is_setup" +PLATFORMS = ["binary_sensor", "sensor"] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -137,8 +139,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): session = hass.data[DOMAIN].pop(entry.entry_id) await session.remove_webhook() - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, platform) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) @@ -186,18 +188,18 @@ class MinutPointClient: async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) return - async def new_device(device_id, component): + async def new_device(device_id, platform): """Load new device.""" - config_entries_key = f"{component}.{DOMAIN}" + config_entries_key = f"{platform}.{DOMAIN}" async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: await self._hass.config_entries.async_forward_entry_setup( - self._config_entry, component + self._config_entry, platform ) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) async_dispatcher_send( - self._hass, POINT_DISCOVERY_NEW.format(component, DOMAIN), device_id + self._hass, POINT_DISCOVERY_NEW.format(platform, DOMAIN), device_id ) self._is_available = True @@ -207,8 +209,8 @@ class MinutPointClient: self._known_homes.add(home_id) for device in self._client.devices: if device.device_id not in self._known_devices: - for component in ("sensor", "binary_sensor"): - await new_device(device.device_id, component) + for platform in PLATFORMS: + await new_device(device.device_id, platform) self._known_devices.add(device.device_id) async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY) @@ -294,7 +296,7 @@ class MinutPointEntity(Entity): return self._id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return status of device.""" attrs = self.device.device_status attrs["last_heard_from"] = as_local(self.last_update).strftime( diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index d82ecd096ee..0bcd2a33a2e 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -1,8 +1,16 @@ """Support for Minut Point binary sensors.""" import logging +from pypoint import EVENTS + from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_SOUND, DOMAIN, BinarySensorEntity, ) @@ -14,37 +22,22 @@ from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK _LOGGER = logging.getLogger(__name__) -EVENTS = { - "battery": ("battery_low", ""), # On means low, Off means normal - "button_press": ( # On means the button was pressed, Off means normal - "short_button_press", - "", - ), - "cold": ( # On means cold, Off means normal - "temperature_low", - "temperature_risen_normal", - ), - "connectivity": ( # On means connected, Off means disconnected - "device_online", - "device_offline", - ), - "dry": ( # On means too dry, Off means normal - "humidity_low", - "humidity_risen_normal", - ), - "heat": ( # On means hot, Off means normal - "temperature_high", - "temperature_dropped_normal", - ), - "moisture": ( # On means wet, Off means dry - "humidity_high", - "humidity_dropped_normal", - ), - "sound": ( # On means sound detected, Off means no sound (clear) - "avg_sound_high", - "sound_level_dropped_normal", - ), - "tamper": ("tamper", ""), # On means the point was removed or attached + +DEVICES = { + "alarm": {"icon": "mdi:alarm-bell"}, + "battery": {"device_class": DEVICE_CLASS_BATTERY}, + "button_press": {"icon": "mdi:gesture-tap-button"}, + "cold": {"device_class": DEVICE_CLASS_COLD}, + "connectivity": {"device_class": DEVICE_CLASS_CONNECTIVITY}, + "dry": {"icon": "mdi:water"}, + "glass": {"icon": "mdi:window-closed-variant"}, + "heat": {"device_class": DEVICE_CLASS_HEAT}, + "moisture": {"device_class": DEVICE_CLASS_MOISTURE}, + "motion": {"device_class": DEVICE_CLASS_MOTION}, + "noise": {"icon": "mdi:volume-high"}, + "sound": {"device_class": DEVICE_CLASS_SOUND}, + "tamper_old": {"icon": "mdi:shield-alert"}, + "tamper": {"icon": "mdi:shield-alert"}, } @@ -56,8 +49,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): client = hass.data[POINT_DOMAIN][config_entry.entry_id] async_add_entities( ( - MinutPointBinarySensor(client, device_id, device_class) - for device_class in EVENTS + MinutPointBinarySensor(client, device_id, device_name) + for device_name in DEVICES + if device_name in EVENTS ), True, ) @@ -70,12 +64,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): """The platform class required by Home Assistant.""" - def __init__(self, point_client, device_id, device_class): + def __init__(self, point_client, device_id, device_name): """Initialize the binary sensor.""" - super().__init__(point_client, device_id, device_class) - + super().__init__( + point_client, + device_id, + DEVICES[device_name].get("device_class"), + ) + self._device_name = device_name self._async_unsub_hook_dispatcher_connect = None - self._events = EVENTS[device_class] + self._events = EVENTS[device_name] self._is_on = None async def async_added_to_hass(self): @@ -124,3 +122,18 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): # connectivity is the other way around. return not self._is_on return self._is_on + + @property + def name(self): + """Return the display name of this device.""" + return f"{self._name} {self._device_name.capitalize()}" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEVICES[self._device_name].get("icon") + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"point.{self._id}-{self._device_name}" diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index aaefc45bc9c..1f3cf2a751d 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -40,7 +40,7 @@ def register_flow_implementation(hass, domain, client_id, client_secret): } -@config_entries.HANDLERS.register("point") +@config_entries.HANDLERS.register(DOMAIN) class PointFlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 6c25cdef91a..899e5615b40 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -3,7 +3,7 @@ "name": "Minut Point", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", - "requirements": ["pypoint==2.0.0"], + "requirements": ["pypoint==2.1.0"], "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], "quality_scale": "gold" diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 87f9a8ab2fe..338ed275f50 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -1,7 +1,7 @@ """Support for Minut Point sensors.""" import logging -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, SensorEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -47,7 +47,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MinutPointSensor(MinutPointEntity): +class MinutPointSensor(MinutPointEntity, SensorEntity): """The platform class required by Home Assistant.""" def __init__(self, point_client, device_id, device_class): diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index 343c02055d5..c36c7a44b51 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -24,7 +24,7 @@ "data": { "flow_impl": "Anbieter" }, - "description": "M\u00f6chtest du mit der Einrichtung beginnen?", + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", "title": "W\u00e4hle die Authentifizierungsmethode" } } diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index c31e2c55e6a..7f4346a6ea9 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -4,7 +4,8 @@ "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" @@ -15,7 +16,7 @@ }, "step": { "auth": { - "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a Fogadd el a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a K\u00fcld\u00e9s gombot. \n\n [Link] ( {authorization_url} )", + "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a **Fogadd el** a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a **K\u00fcld\u00e9s ** gombot. \n\n [Link]({authorization_url})", "title": "Point hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/point/translations/id.json b/homeassistant/components/point/translations/id.json new file mode 100644 index 00000000000..868321d7469 --- /dev/null +++ b/homeassistant/components/point/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_setup": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "external_setup": "Point berhasil dikonfigurasi dari alur konfigurasi lainnya.", + "no_flows": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "error": { + "follow_link": "Buka tautan dan autentikasi sebelum menekan Kirim", + "no_token": "Token akses tidak valid" + }, + "step": { + "auth": { + "description": "Buka tautan di bawah ini dan **Terima** akses ke akun Minut Anda, lalu kembali dan tekan tombol **Kirim** di bawah ini.\n\n[Tautan] ({authorization_url})", + "title": "Autentikasi Point" + }, + "user": { + "data": { + "flow_impl": "Penyedia" + }, + "description": "Ingin memulai penyiapan?", + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index f4ca6002036..5813dbba137 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "already_setup": "\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.", + "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "external_setup": "Point \uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "external_setup": "Point\uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index 94763a412a0..8447ac6bbb2 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -1,31 +1,31 @@ { "config": { "abort": { - "already_setup": "U kunt alleen een Point-account configureren.", + "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", "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": "U moet Point configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/).", + "no_flows": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { - "default": "Succesvol geverifieerd met Minut voor uw Point appara(a)t(en)" + "default": "Succesvol geauthenticeerd" }, "error": { "follow_link": "Volg de link en verifieer voordat je op Verzenden klikt", - "no_token": "Niet geverifieerd met Minut" + "no_token": "Ongeldig toegangstoken" }, "step": { "auth": { - "description": "Ga naar onderstaande link en Accepteer toegang tot je Minut account, kom dan hier terug en klik op Verzenden hier onder.\n\n[Link]({authorization_url})", + "description": "Ga naar onderstaande link en **Accepteer** toegang tot je Minut account, kom dan hier terug en klik op **Verzenden** hier onder.\n\n[Link]({authorization_url})", "title": "Verificatie Point" }, "user": { "data": { "flow_impl": "Leverancier" }, - "description": "Kies met welke authenticatieprovider u wilt authenticeren met Point.", - "title": "Authenticatieleverancier" + "description": "Wil je beginnen met instellen?", + "title": "Kies een authenticatie methode" } } } diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 315816f4e1c..cfc2abb0316 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -10,7 +10,6 @@ from poolsense.exceptions import PoolSenseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -49,16 +48,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = PoolSenseDataUpdateCoordinator(hass, entry) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -69,8 +65,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -118,7 +114,7 @@ class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): try: data = await self.poolsense.get_poolsense_data() except (PoolSenseError) as error: - _LOGGER.error("PoolSense query did not complete.") + _LOGGER.error("PoolSense query did not complete") raise UpdateFailed(error) from error return data diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py index b9c73ace3fc..653ba026ebf 100644 --- a/homeassistant/components/poolsense/config_flow.py +++ b/homeassistant/components/poolsense/config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index a64cc0aef61..ca79fde6b08 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for the PoolSense sensor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_EMAIL, @@ -8,7 +9,6 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity from . import PoolSenseEntity from .const import ATTRIBUTION, DOMAIN @@ -79,7 +79,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors_list, False) -class PoolSenseSensor(PoolSenseEntity, Entity): +class PoolSenseSensor(PoolSenseEntity, SensorEntity): """Sensor representing poolsense data.""" @property @@ -108,6 +108,6 @@ class PoolSenseSensor(PoolSenseEntity, Entity): return SENSORS[self.info_type]["unit"] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 5869da61c9c..6b64ec2eef1 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -12,7 +12,7 @@ "email": "E-Mail", "password": "Passwort" }, - "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/poolsense/translations/hu.json b/homeassistant/components/poolsense/translations/hu.json index 3b2d79a34a7..80562b34e28 100644 --- a/homeassistant/components/poolsense/translations/hu.json +++ b/homeassistant/components/poolsense/translations/hu.json @@ -2,6 +2,19 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + }, + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "title": "PoolSense" + } } } } \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/id.json b/homeassistant/components/poolsense/translations/id.json new file mode 100644 index 00000000000..6e40f5f0925 --- /dev/null +++ b/homeassistant/components/poolsense/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + }, + "description": "Ingin memulai penyiapan?", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/nl.json b/homeassistant/components/poolsense/translations/nl.json index 38ef34d5afc..f88d14e297a 100644 --- a/homeassistant/components/poolsense/translations/nl.json +++ b/homeassistant/components/poolsense/translations/nl.json @@ -12,7 +12,8 @@ "email": "E-mail", "password": "Wachtwoord" }, - "description": "Wil je beginnen met instellen?" + "description": "Wil je beginnen met instellen?", + "title": "PoolSense" } } } diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index b392b713741..ceec56aa05a 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -11,7 +11,7 @@ from tesla_powerwall import ( PowerwallUnreachableError, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -156,11 +156,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): } ) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -185,7 +185,7 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=entry.data, ) ) @@ -228,8 +228,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index eb804df3420..579c916a15a 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import callback -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 5026d2fb357..36f803e66d7 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,6 +3,7 @@ import logging from tesla_powerwall import MeterType +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, PERCENTAGE from .const import ( @@ -59,7 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class PowerWallChargeSensor(PowerWallEntity): +class PowerWallChargeSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall charge sensor.""" @property @@ -88,7 +89,7 @@ class PowerWallChargeSensor(PowerWallEntity): return round(self.coordinator.data[POWERWALL_API_CHARGE]) -class PowerWallEnergySensor(PowerWallEntity): +class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" def __init__( @@ -136,7 +137,7 @@ class PowerWallEnergySensor(PowerWallEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" meter = self.coordinator.data[POWERWALL_API_METERS].get_meter(self._meter) return { diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json new file mode 100644 index 00000000000..cef3726d759 --- /dev/null +++ b/homeassistant/components/powerwall/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 7cc0ceafac1..a2b8af13370 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { - "ip_address": "IP-c\u00edm" + "ip_address": "IP c\u00edm", + "password": "Jelsz\u00f3" } } } diff --git a/homeassistant/components/powerwall/translations/id.json b/homeassistant/components/powerwall/translations/id.json new file mode 100644 index 00000000000..a5ae5f5e979 --- /dev/null +++ b/homeassistant/components/powerwall/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan", + "wrong_version": "Powerwall Anda menggunakan versi perangkat lunak yang tidak didukung. Pertimbangkan untuk memutakhirkan atau melaporkan masalah ini agar dapat diatasi." + }, + "flow_title": "Tesla Powerwall ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "password": "Kata Sandi" + }, + "description": "Kata sandi umumnya adalah 5 karakter terakhir dari nomor seri untuk Backup Gateway dan dapat ditemukan di aplikasi Tesla atau 5 karakter terakhir kata sandi yang ditemukan di dalam pintu untuk Backup Gateway 2.", + "title": "Hubungkan ke powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/ko.json b/homeassistant/components/powerwall/translations/ko.json index 11c638fa9bd..d1dd99bbb7a 100644 --- a/homeassistant/components/powerwall/translations/ko.json +++ b/homeassistant/components/powerwall/translations/ko.json @@ -10,13 +10,15 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "wrong_version": "Powerwall \uc774 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ubc84\uc804\uc758 \uc18c\ud504\ud2b8\uc6e8\uc5b4\ub97c \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4. \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud558\ub824\uba74 \uc5c5\uadf8\ub808\uc774\ub4dc\ud558\uac70\ub098 \uc774 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { "ip_address": "IP \uc8fc\uc18c", "password": "\ube44\ubc00\ubc88\ud638" }, - "title": "powerwall \uc5d0 \uc5f0\uacb0\ud558\uae30" + "description": "\ube44\ubc00\ubc88\ud638\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \ubc31\uc5c5 \uac8c\uc774\ud2b8\uc6e8\uc774 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc758 \ub9c8\uc9c0\ub9c9 5\uc790\ub9ac\uc774\uba70 Tesla \uc571 \ub610\ub294 \ubc31\uc5c5 \uac8c\uc774\ud2b8\uc6e8\uc774 2\uc758 \ub3c4\uc5b4 \ub0b4\ubd80\uc5d0 \uc788\ub294 \ub9c8\uc9c0\ub9c9 5\uc790\ub9ac \ube44\ubc00\ubc88\ud638\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "powerwall\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index c4ae2616f46..5149f391669 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "De powerwall is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout", "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." @@ -17,6 +17,7 @@ "ip_address": "IP-adres", "password": "Wachtwoord" }, + "description": "Het wachtwoord is meestal de laatste 5 tekens van het serienummer voor Backup Gateway en is te vinden in de Tesla-app of de laatste 5 tekens van het wachtwoord aan de binnenkant van de deur voor Backup Gateway 2.", "title": "Maak verbinding met de powerwall" } } diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py index 9a71b64a179..259c300239c 100644 --- a/homeassistant/components/profiler/config_flow.py +++ b/homeassistant/components/profiler/config_flow.py @@ -3,8 +3,7 @@ import voluptuous as vol from homeassistant import config_entries -from .const import DEFAULT_NAME -from .const import DOMAIN # pylint: disable=unused-import +from .const import DEFAULT_NAME, DOMAIN class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/profiler/translations/hu.json b/homeassistant/components/profiler/translations/hu.json index bbdd2e5b536..c5d28903888 100644 --- a/homeassistant/components/profiler/translations/hu.json +++ b/homeassistant/components/profiler/translations/hu.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "user": { - "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/profiler/translations/id.json b/homeassistant/components/profiler/translations/id.json new file mode 100644 index 00000000000..3521bb95412 --- /dev/null +++ b/homeassistant/components/profiler/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/ko.json b/homeassistant/components/profiler/translations/ko.json index 0c38052b826..251c9777b6c 100644 --- a/homeassistant/components/profiler/translations/ko.json +++ b/homeassistant/components/profiler/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "user": { diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 02418c963d4..7597b2ff1a2 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -30,9 +30,9 @@ 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 component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -43,8 +43,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json index 3b2d79a34a7..f6f6e2c15b7 100644 --- a/homeassistant/components/progettihwsw/translations/hu.json +++ b/homeassistant/components/progettihwsw/translations/hu.json @@ -2,6 +2,32 @@ "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" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e9 1", + "relay_10": "Rel\u00e9 10", + "relay_11": "Rel\u00e9 11", + "relay_12": "Rel\u00e9 12", + "relay_13": "Rel\u00e9 13", + "relay_14": "Rel\u00e9 14", + "relay_15": "Rel\u00e9 15", + "relay_16": "Rel\u00e9 16", + "relay_2": "Rel\u00e9 2", + "relay_3": "Rel\u00e9 3" + } + }, + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/id.json b/homeassistant/components/progettihwsw/translations/id.json new file mode 100644 index 00000000000..0aa69cad958 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Relai 1", + "relay_10": "Relai 10", + "relay_11": "Relai 11", + "relay_12": "Relai 12", + "relay_13": "Relai 13", + "relay_14": "Relai 14", + "relay_15": "Relai 15", + "relay_16": "Relai 16", + "relay_2": "Relai 2", + "relay_3": "Relai 3", + "relay_4": "Relai 4", + "relay_5": "Relai 5", + "relay_6": "Relai 6", + "relay_7": "Relai 7", + "relay_8": "Relai 8", + "relay_9": "Relai 9" + }, + "title": "Siapkan relai" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Siapkan papan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ko.json b/homeassistant/components/progettihwsw/translations/ko.json index ab21d8427bd..9324783f5cb 100644 --- a/homeassistant/components/progettihwsw/translations/ko.json +++ b/homeassistant/components/progettihwsw/translations/ko.json @@ -27,14 +27,14 @@ "relay_8": "\ub9b4\ub808\uc774 8", "relay_9": "\ub9b4\ub808\uc774 9" }, - "title": "\ub9b4\ub808\uc774 \uc124\uc815" + "title": "\ub9b4\ub808\uc774 \uc124\uc815\ud558\uae30" }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" }, - "title": "\ubcf4\ub4dc \uc124\uc815" + "title": "\ubcf4\ub4dc \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/progettihwsw/translations/nl.json b/homeassistant/components/progettihwsw/translations/nl.json index 7810a8018a4..64eb0d12717 100644 --- a/homeassistant/components/progettihwsw/translations/nl.json +++ b/homeassistant/components/progettihwsw/translations/nl.json @@ -33,7 +33,8 @@ "data": { "host": "Host", "port": "Poort" - } + }, + "title": "Stel het bord in" } } } diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 5dff4725ea0..a293642038e 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -84,7 +84,7 @@ class ProliphixThermostat(ClimateEntity): return PRECISION_TENTHS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" return {ATTR_FAN: self._pdp.fan_state} diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index bd9a6e35276..b253daf559e 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -16,12 +16,12 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier.const import ( ATTR_AVAILABLE_MODES, ATTR_HUMIDITY, - ATTR_MODE, ) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, + ATTR_MODE, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, @@ -29,6 +29,7 @@ from homeassistant.const import ( PERCENTAGE, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -154,9 +155,11 @@ class PrometheusMetrics: if not self._filter(state.entity_id): return + ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN) + handler = f"_handle_{domain}" - if hasattr(self, handler) and state.state != STATE_UNAVAILABLE: + if hasattr(self, handler) and state.state not in ignored_states: getattr(self, handler)(state) labels = self._labels(state) @@ -168,9 +171,9 @@ class PrometheusMetrics: entity_available = self._metric( "entity_available", self.prometheus_cli.Gauge, - "Entity is available (not in the unavailable state)", + "Entity is available (not in the unavailable or unknown state)", ) - entity_available.labels(**labels).set(float(state.state != STATE_UNAVAILABLE)) + entity_available.labels(**labels).set(float(state.state not in ignored_states)) last_updated_time_seconds = self._metric( "last_updated_time_seconds", diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 5d2efe76607..725c3b9de30 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -48,6 +48,8 @@ class ProwlNotificationService(BaseNotificationService): "description": message, "priority": data["priority"] if data and "priority" in data else 0, } + if data.get("url"): + payload["url"] = data["url"] _LOGGER.debug("Attempting call Prowl service at %s", url) session = async_get_clientsession(self._hass) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 6df0f50b720..de9d6247f9f 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -148,7 +148,7 @@ class Proximity(Entity): return self._unit_of_measurement @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_DIR_OF_TRAVEL: self.dir_of_travel, ATTR_NEAREST: self.nearest} diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 2f42ca8fe9e..a149c8b6034 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -185,12 +185,12 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data[DOMAIN][COORDINATOR] = coordinator # Fetch initial data - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() - for component in PLATFORMS: + for platform in PLATFORMS: await hass.async_create_task( hass.helpers.discovery.async_load_platform( - component, DOMAIN, {"config": config}, config + platform, DOMAIN, {"config": config}, config ) ) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index c3f7151431a..8fda507ace2 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -77,7 +77,7 @@ def _precheck_image(image, opts): if imgfmt not in ("PNG", "JPEG"): _LOGGER.warning("Image is of unsupported type: %s", imgfmt) raise ValueError() - if not img.mode == "RGB": + if img.mode != "RGB": img = img.convert("RGB") return img diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index c1a01004fe9..86f3d23d308 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==8.1.1"], + "requirements": ["pillow==8.1.2"], "codeowners": [] } diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 390637c26a3..11d271be543 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json -from .config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import +from .config_flow import PlayStation4FlowHandler # noqa: F401 from .const import ATTR_MEDIA_IMAGE_URL, COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 24a1589db0d..be77ea04f1c 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,5 +1,6 @@ """Support for PlayStation 4 consoles.""" import asyncio +from contextlib import suppress import logging from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete @@ -142,10 +143,8 @@ class PS4Device(MediaPlayerEntity): and not self._ps4.is_standby and self._ps4.is_available ): - try: + with suppress(NotReady): await self._ps4.async_connect() - except NotReady: - pass # Try to ensure correct status is set on startup for device info. if self._ps4.ddp_protocol is None: diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json index c3bcabb0d3a..bb677b21700 100644 --- a/homeassistant/components/ps4/translations/hu.json +++ b/homeassistant/components/ps4/translations/hu.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a helyes-e." + "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a PIN-k\u00f3d helyes-e." }, "step": { "creds": { @@ -10,14 +14,15 @@ }, "link": { "data": { - "code": "PIN", - "ip_address": "IP-c\u00edm", + "code": "PIN-k\u00f3d", + "ip_address": "IP c\u00edm", "name": "N\u00e9v", "region": "R\u00e9gi\u00f3" } }, "mode": { "data": { + "ip_address": "IP c\u00edm (Hagyd \u00fcresen az Automatikus Felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz).", "mode": "Konfigur\u00e1ci\u00f3s m\u00f3d" }, "title": "PlayStation 4" diff --git a/homeassistant/components/ps4/translations/id.json b/homeassistant/components/ps4/translations/id.json new file mode 100644 index 00000000000..aab31564e16 --- /dev/null +++ b/homeassistant/components/ps4/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "credential_error": "Terjadi kesalahan saat mengambil kredensial.", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "port_987_bind_error": "Tidak dapat mengaitkan ke port 987. Baca [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut.", + "port_997_bind_error": "Tidak dapat mengaitkan ke port 997. Baca [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "credential_timeout": "Tenggang waktu layanan kredensial habis. Tekan kirim untuk memulai kembali.", + "login_failed": "Gagal memasangkan dengan PlayStation 4. Pastikan Kode PIN sudah benar.", + "no_ipaddress": "Masukkan Alamat IP perangkat PlayStation 4 yang dikonfigurasi." + }, + "step": { + "creds": { + "description": "Kredensial diperlukan. Tekan 'Kirim' dan kemudian di Aplikasi Layar ke-2 PS4, segarkan perangkat dan pilih perangkat 'Home-Assistant' untuk melanjutkan.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "Kode PIN", + "ip_address": "Alamat IP", + "name": "Nama", + "region": "Wilayah" + }, + "description": "Masukkan informasi PlayStation 4 Anda. Untuk Kode PIN, buka 'Pengaturan' di konsol PlayStation 4 Anda. Kemudian buka 'Pengaturan Koneksi Aplikasi Seluler' dan pilih 'Tambah Perangkat'. Masukkan Kode PIN yang ditampilkan. Lihat [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut.", + "title": "PlayStation 4" + }, + "mode": { + "data": { + "ip_address": "Alamat IP (Kosongkan jika menggunakan Penemuan Otomatis).", + "mode": "Mode Konfigurasi" + }, + "description": "Pilih mode untuk konfigurasi. Alamat IP dapat dikosongkan jika memilih Penemuan Otomatis karena perangkat akan ditemukan secara otomatis.", + "title": "PlayStation 4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/ko.json b/homeassistant/components/ps4/translations/ko.json index 76762a0bec6..129927a9bab 100644 --- a/homeassistant/components/ps4/translations/ko.json +++ b/homeassistant/components/ps4/translations/ko.json @@ -4,8 +4,8 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "port_987_bind_error": "987 \ud3ec\ud2b8\uc5d0 \ubc14\uc778\ub529\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "port_997_bind_error": "997 \ud3ec\ud2b8\uc5d0 \ubc14\uc778\ub529\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c\uace0\uce68 \ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "\uc774\ub984", "region": "\uc9c0\uc5ed" }, - "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. PIN \ucf54\ub4dc\ub97c \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30' \ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. PIN \ucf54\ub4dc\ub97c \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30'\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc(\uc744)\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/translations/nl.json b/homeassistant/components/ps4/translations/nl.json index 326917e4960..db59c7ab30a 100644 --- a/homeassistant/components/ps4/translations/nl.json +++ b/homeassistant/components/ps4/translations/nl.json @@ -3,15 +3,15 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "credential_error": "Fout bij ophalen van inloggegevens.", - "no_devices_found": "Geen PlayStation 4 apparaten gevonden op het netwerk.", + "no_devices_found": "Geen apparaten gevonden op het netwerk", "port_987_bind_error": "Kon niet binden aan poort 987. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor meer informatie.", "port_997_bind_error": "Kon niet binden aan poort 997. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor aanvullende informatie." }, "error": { "cannot_connect": "Kan geen verbinding maken", "credential_timeout": "Time-out van inlog service. Druk op Submit om opnieuw te starten.", - "login_failed": "Kan niet koppelen met PlayStation 4. Controleer of de pincode juist is.", - "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die je wilt configureren." + "login_failed": "Kan Playstation 4 niet koppelen. Controleer of de PIN-code correct is.", + "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die u wilt configureren." }, "step": { "creds": { @@ -20,20 +20,20 @@ }, "link": { "data": { - "code": "PIN", + "code": "PIN-code", "ip_address": "IP-adres", "name": "Naam", "region": "Regio" }, - "description": "Voer je PlayStation 4-informatie in. Ga voor 'PIN' naar 'Instellingen' op je PlayStation 4-console. Navigeer vervolgens naar 'Verbindingsinstellingen mobiele app' en selecteer 'Apparaat toevoegen'. Voer de pincode in die wordt weergegeven. Raadpleeg de [documentatie] (https://www.home-assistant.io/components/ps4/) voor meer informatie.", + "description": "Voer je PlayStation 4-informatie in. Voor PIN-code navigeer je naar 'Instellingen' op je PlayStation 4-console. Ga vervolgens naar 'Verbindingsinstellingen mobiele app' en selecteer 'Apparaat toevoegen'. Voer de PIN-code in die wordt weergegeven. Raadpleeg de [documentatie](https://www.home-assistant.io/components/ps4/) voor meer informatie.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "IP-adres (leeg laten als u Auto Discovery gebruikt).", + "ip_address": "IP-adres (leeg laten indien Auto Discovery wordt gebruikt).", "mode": "Configuratiemodus" }, - "description": "Selecteer modus voor configuratie. Het veld IP-adres kan leeg blijven als Auto Discovery wordt geselecteerd, omdat apparaten automatisch worden gedetecteerd.", + "description": "Selecteer modus voor configuratie. Het veld IP-adres kan leeg worden gelaten als Auto Discovery wordt geselecteerd, omdat apparaten dan automatisch worden ontdekt.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 9c27ab4e027..260fbe65c1b 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -73,7 +73,7 @@ class PALoopbackSwitch(SwitchEntity): self._pa_svr.connect() for module in self._pa_svr.module_list(): - if not module.name == "module-loopback": + if module.name != "module-loopback": continue if f"sink={self._sink_name}" not in module.argument: diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 31f2f88dac7..ff0ac45c139 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -175,7 +175,7 @@ class PushCamera(Camera): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { name: value diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index ff18e86aad9..4f8ec6a1700 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -5,10 +5,9 @@ import threading from pushbullet import InvalidKeyError, Listener, PushBullet import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class PushBulletNotificationSensor(Entity): +class PushBulletNotificationSensor(SensorEntity): """Representation of a Pushbullet Sensor.""" def __init__(self, pb, element): @@ -85,7 +84,7 @@ class PushBulletNotificationSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return all known attributes of the sensor.""" return self._state_attributes diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 12735764b4b..3337af0f8b0 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -15,7 +15,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import HTTP_OK +from homeassistant.const import ATTR_ICON, HTTP_OK import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,6 @@ CONF_TIMEOUT = 15 # Top level attributes in 'data' ATTR_SOUND = "sound" ATTR_VIBRATION = "vibration" -ATTR_ICON = "icon" ATTR_ICONCOLOR = "iconcolor" ATTR_URL = "url" ATTR_URLTITLE = "urltitle" @@ -94,7 +93,7 @@ class PushsaferNotificationService(BaseNotificationService): _LOGGER.debug("Loading image from file %s", local_path) picture1_encoded = self.load_from_file(local_path) else: - _LOGGER.warning("missing url or local_path for picture1") + _LOGGER.warning("Missing url or local_path for picture1") else: _LOGGER.debug("picture1 is not specified") @@ -144,7 +143,7 @@ class PushsaferNotificationService(BaseNotificationService): else: response = requests.get(url, timeout=CONF_TIMEOUT) return self.get_base64(response.content, response.headers["content-type"]) - _LOGGER.warning("url not found in param") + _LOGGER.warning("No url was found in param") return None diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 32d33f19e80..eb461061dcc 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol from homeassistant.components.rest.data import RestData -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_DATE, ATTR_TEMPERATURE, @@ -17,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" @@ -64,7 +63,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([PvoutputSensor(rest, name)]) -class PvoutputSensor(Entity): +class PvoutputSensor(SensorEntity): """Representation of a PVOutput sensor.""" def __init__(self, rest, name): @@ -100,7 +99,7 @@ class PvoutputSensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the monitored installation.""" if self.pvcoutput is not None: return { diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index a9b53c970bd..5fe65e3dc65 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -1,11 +1,13 @@ """Sensor to collect the reference daily prices of electricity ('PVPC') in Spain.""" +from __future__ import annotations + import logging from random import randint -from typing import Optional from aiopvpc import PVPCData from homeassistant import config_entries +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, CURRENCY_EURO, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -41,7 +43,7 @@ async def async_setup_entry( ) -class ElecPriceSensor(RestoreEntity): +class ElecPriceSensor(RestoreEntity, SensorEntity): """Class to hold the prices of electricity as a sensor.""" unit_of_measurement = UNIT @@ -92,7 +94,7 @@ class ElecPriceSensor(RestoreEntity): self.update_current_price(dt_util.utcnow()) @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._unique_id @@ -112,7 +114,7 @@ class ElecPriceSensor(RestoreEntity): return self._pvpc_data.state_available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._pvpc_data.attributes diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json new file mode 100644 index 00000000000..f5301e874ea --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/id.json b/homeassistant/components/pvpc_hourly_pricing/translations/id.json new file mode 100644 index 00000000000..8601c31fda0 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "name": "Nama Sensor", + "tariff": "Tarif kontrak (1, 2, atau 3 periode)" + }, + "description": "Sensor ini menggunakan API resmi untuk mendapatkan [harga listrik per jam (PVPC)](https://www.esios.ree.es/es/pvpc) di Spanyol.\nUntuk penjelasan yang lebih tepat, kunjungi [dokumen integrasi](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nPilih tarif kontrak berdasarkan jumlah periode penagihan per hari:\n- 1 periode: normal\n- 2 periode: diskriminasi (tarif per malam)\n- 3 periode: mobil listrik (tarif per malam 3 periode)", + "title": "Pemilihan tarif" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json index f1f225ae525..c44d1217961 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json @@ -9,7 +9,7 @@ "name": "\uc13c\uc11c \uc774\ub984", "tariff": "\uacc4\uc57d \uc694\uae08\uc81c (1, 2 \ub610\ub294 3 \uad6c\uac04)" }, - "description": "\uc774 \uc13c\uc11c\ub294 \uacf5\uc2dd API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud398\uc778\uc758 [\uc2dc\uac04\ub2f9 \uc804\uae30 \uc694\uae08 (PVPC)](https://www.esios.ree.es/es/pvpc) \uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\n\ubcf4\ub2e4 \uc790\uc138\ud55c \uc124\uba85\uc740 [\uc548\ub0b4](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.\n\n1\uc77c\ub2f9 \uccad\uad6c \uad6c\uac04\uc5d0 \ub530\ub77c \uacc4\uc57d \uc694\uae08\uc81c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\n - 1 \uad6c\uac04: \uc77c\ubc18 \uc694\uae08\uc81c\n - 2 \uad6c\uac04: \ucc28\ub4f1 \uc694\uae08\uc81c (\uc57c\uac04 \uc694\uae08) \n - 3 \uad6c\uac04: \uc804\uae30\uc790\ub3d9\ucc28 (3 \uad6c\uac04 \uc57c\uac04 \uc694\uae08)", + "description": "\uc774 \uc13c\uc11c\ub294 \uacf5\uc2dd API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud398\uc778\uc758 [\uc2dc\uac04\ub2f9 \uc804\uae30 \uc694\uae08 (PVPC)](https://www.esios.ree.es/es/pvpc) \uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\n\ubcf4\ub2e4 \uc790\uc138\ud55c \uc124\uba85\uc740 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.\n\n1\uc77c\ub2f9 \uccad\uad6c \uad6c\uac04\uc5d0 \ub530\ub77c \uacc4\uc57d \uc694\uae08\uc81c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\n - 1 \uad6c\uac04: \uc77c\ubc18 \uc694\uae08\uc81c\n - 2 \uad6c\uac04: \ucc28\ub4f1 \uc694\uae08\uc81c (\uc57c\uac04 \uc694\uae08) \n - 3 \uad6c\uac04: \uc804\uae30\uc790\ub3d9\ucc28 (3 \uad6c\uac04 \uc57c\uac04 \uc694\uae08)", "title": "\uc694\uae08\uc81c \uc120\ud0dd" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index 3abffdf5bc0..5048ed498df 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met dat tarief" + "already_configured": "Service is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 6539479d2cd..c439d5181be 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -6,7 +6,7 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -19,7 +19,6 @@ from homeassistant.const import ( DATA_RATE_MEGABYTES_PER_SECOND, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class PyLoadSensor(Entity): +class PyLoadSensor(SensorEntity): """Representation of a pyLoad sensor.""" def __init__(self, api, sensor_type, client_name): diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index bed159cae6b..2051b32b63f 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -19,7 +19,7 @@ from RestrictedPython.Guards import ( ) import voluptuous as vol -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import CONF_DESCRIPTION, CONF_NAME, SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass @@ -71,6 +71,8 @@ ALLOWED_DT_UTIL = { "get_age", } +CONF_FIELDS = "fields" + class ScriptError(HomeAssistantError): """When a script error occurs.""" @@ -125,8 +127,9 @@ def discover_scripts(hass): hass.services.register(DOMAIN, name, python_script_service_handler) service_desc = { - "description": services_dict.get(name, {}).get("description", ""), - "fields": services_dict.get(name, {}).get("fields", {}), + CONF_NAME: services_dict.get(name, {}).get("name", name), + CONF_DESCRIPTION: services_dict.get(name, {}).get("description", ""), + CONF_FIELDS: services_dict.get(name, {}).get("fields", {}), } async_set_service_schema(hass, DOMAIN, name, service_desc) diff --git a/homeassistant/components/python_script/services.yaml b/homeassistant/components/python_script/services.yaml index 835f6402481..e9f860f1a62 100644 --- a/homeassistant/components/python_script/services.yaml +++ b/homeassistant/components/python_script/services.yaml @@ -1,4 +1,5 @@ # Describes the format for available python_script services reload: + name: Reload description: Reload all available python_scripts diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index cd67355d883..251407099b1 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -5,7 +5,7 @@ from qbittorrent.client import Client, LoginRequired from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -71,7 +70,7 @@ def format_speed(speed): return round(kb_spd, 2 if kb_spd < 0.1 else 1) -class QBittorrentSensor(Entity): +class QBittorrentSensor(SensorEntity): """Representation of an qBittorrent sensor.""" def __init__(self, sensor_type, qbittorrent_client, client_name, exception): diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index f608f6e12ae..669e9d1e884 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -1,7 +1,8 @@ """Support for Queensland Bushfire Alert Feeds.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional from georss_qld_bushfire_alert_client import QldBushfireAlertFeedManager import voluptuous as vol @@ -209,22 +210,22 @@ class QldBushfireLocationEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -234,7 +235,7 @@ class QldBushfireLocationEvent(GeolocationEvent): return LENGTH_KILOMETERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index b83d37f0d3c..29750683abf 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -2,6 +2,6 @@ "domain": "qnap", "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", - "requirements": ["qnapstats==0.3.0"], + "requirements": ["qnapstats==0.3.1"], "codeowners": ["@colinodell"] } diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 11faba0f210..5759713e80c 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -5,7 +5,7 @@ import logging from qnapstats import QNAPStats import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_NAME, CONF_HOST, @@ -23,7 +23,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -200,7 +199,7 @@ class QNAPStatsAPI: _LOGGER.exception("Failed to fetch QNAP stats from the NAS") -class QNAPSensor(Entity): +class QNAPSensor(SensorEntity): """Base class for a QNAP sensor.""" def __init__(self, api, variable, variable_info, monitor_device=None): @@ -268,7 +267,7 @@ class QNAPMemorySensor(QNAPSensor): return round(used / total * 100) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._api.data: data = self._api.data["system_stats"]["memory"] @@ -294,7 +293,7 @@ class QNAPNetworkSensor(QNAPSensor): return round_nicely(data["rx"] / 1024 / 1024) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._api.data: data = self._api.data["system_stats"]["nics"][self.monitor_device] @@ -322,7 +321,7 @@ class QNAPSystemSensor(QNAPSensor): return int(self._api.data["system_stats"]["system"]["temp_c"]) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._api.data: data = self._api.data["system_stats"] @@ -360,7 +359,7 @@ class QNAPDriveSensor(QNAPSensor): return f"{server_name} {self.var_name} (Drive {self.monitor_device})" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._api.data: data = self._api.data["smart_drive_health"][self.monitor_device] @@ -394,7 +393,7 @@ class QNAPVolumeSensor(QNAPSensor): return round(used_gb / total_gb * 100) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._api.data: data = self._api.data["volumes"][self.monitor_device] diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 5867d0d6b51..bd574af0297 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,6 +2,6 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==8.1.1", "pyzbar==0.1.7"], + "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"], "codeowners": [] } diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 9dd8e3c4f20..2f4353063d1 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -82,7 +82,7 @@ class QVRProCamera(Camera): return self._brand @property - def device_state_attributes(self): + def extra_state_attributes(self): """Get the state attributes.""" attrs = {"qvr_guid": self.guid} diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 53cf68ccdba..f6d0ce7ec28 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -3,6 +3,7 @@ import logging from pyqwikswitch.qwikswitch import SENSORS +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from . import DOMAIN as QWIKSWITCH, QSEntity @@ -21,7 +22,7 @@ async def async_setup_platform(hass, _, add_entities, discovery_info=None): add_entities(devs) -class QSSensor(QSEntity): +class QSSensor(QSEntity, SensorEntity): """Sensor based on a Qwikswitch relay/dimmer module.""" _val = None diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 672ff272344..30015dcf8c1 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -21,7 +21,7 @@ from .webhooks import ( _LOGGER = logging.getLogger(__name__) -SUPPORTED_DOMAINS = ["switch", "binary_sensor"] +PLATFORMS = ["switch", "binary_sensor"] CONFIG_SCHEMA = cv.deprecated(DOMAIN) @@ -39,8 +39,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SUPPORTED_DOMAINS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -99,13 +99,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): webhook_url, ) - # Enable component + # Enable platform hass.data[DOMAIN][entry.entry_id] = person async_register_webhook(hass, webhook_id, entry.entry_id) - for component in SUPPORTED_DOMAINS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 63e5bd56954..5719dd81066 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -12,11 +12,11 @@ from homeassistant.core import callback from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, + DOMAIN, KEY_ID, KEY_STATUS, KEY_USERNAME, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index c9de7eea7d4..a6ed596db04 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -1,6 +1,7 @@ """Adapter to wrap the rachiopy api for home assistant.""" +from __future__ import annotations + import logging -from typing import Optional import voluptuous as vol @@ -239,7 +240,7 @@ class RachioIro: # Only enabled zones return [z for z in self._zones if z[KEY_ENABLED]] - def get_zone(self, zone_id) -> Optional[dict]: + def get_zone(self, zone_id) -> dict | None: """Return the zone with the given ID.""" for zone in self.list_zones(include_disabled=True): if zone[KEY_ID] == zone_id: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 44a17acaecf..8d87b688aa4 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -1,5 +1,6 @@ """Integration with the Rachio Iro sprinkler system controller.""" from abc import abstractmethod +from contextlib import suppress from datetime import timedelta import logging @@ -388,7 +389,7 @@ class RachioZone(RachioSwitch): return self._entity_picture @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the optional state attributes.""" props = {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary} if self._shade_type: @@ -494,7 +495,7 @@ class RachioSchedule(RachioSwitch): return "mdi:water" if self.schedule_is_enabled else "mdi:water-off" @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the optional state attributes.""" return { ATTR_SCHEDULE_SUMMARY: self._summary, @@ -525,7 +526,7 @@ class RachioSchedule(RachioSwitch): def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook schedule data.""" # Schedule ID not passed when running individual zones, so we catch that error - try: + with suppress(KeyError): if args[0][KEY_SCHEDULE_ID] == self._schedule_id: if args[0][KEY_SUBTYPE] in [SUBTYPE_SCHEDULE_STARTED]: self._state = True @@ -534,8 +535,6 @@ class RachioSchedule(RachioSwitch): SUBTYPE_SCHEDULE_COMPLETED, ]: self._state = False - except KeyError: - pass self.async_write_ha_state() diff --git a/homeassistant/components/rachio/translations/hu.json b/homeassistant/components/rachio/translations/hu.json index 5f4f7bb8bee..570dd27b5d9 100644 --- a/homeassistant/components/rachio/translations/hu.json +++ b/homeassistant/components/rachio/translations/hu.json @@ -1,5 +1,13 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rachio/translations/id.json b/homeassistant/components/rachio/translations/id.json new file mode 100644 index 00000000000..66302734248 --- /dev/null +++ b/homeassistant/components/rachio/translations/id.json @@ -0,0 +1,30 @@ +{ + "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": { + "api_key": "Kunci API" + }, + "description": "Anda akan memerlukan Kunci API dari https://app.rach.io/. Buka Settings, lalu klik 'GET API KEY'.", + "title": "Hubungkan ke perangkat Rachio Anda" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Durasi dalam menit yang akan dijalankan saat mengaktifkan sakelar zona" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/ko.json b/homeassistant/components/rachio/translations/ko.json index 298ef476745..e0c7a3a13a3 100644 --- a/homeassistant/components/rachio/translations/ko.json +++ b/homeassistant/components/rachio/translations/ko.json @@ -13,7 +13,7 @@ "data": { "api_key": "API \ud0a4" }, - "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. Settings \ub85c \uc774\ub3d9\ud55c \ub2e4\uc74c 'GET API KEY ' \ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", + "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. Settings\ub85c \uc774\ub3d9\ud55c \ub2e4\uc74c 'GET API KEY '\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", "title": "Rachio \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" } } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "\uad6c\uc5ed \uc2a4\uc704\uce58\ub97c \ud65c\uc131\ud654\ud560 \ub54c \uc2e4\ud589\ud560 \uc2dc\uac04(\ubd84)" + "manual_run_mins": "\uad6c\uc5ed \uc2a4\uc704\uce58\ub97c \ud65c\uc131\ud654\ud560 \ub54c \uc2e4\ud589\ud560 \uc2dc\uac04 (\ubd84)" } } } diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json index 2173c768185..6a94ac2dcd4 100644 --- a/homeassistant/components/rachio/translations/nl.json +++ b/homeassistant/components/rachio/translations/nl.json @@ -4,14 +4,14 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "api_key": "De API-sleutel voor het Rachio-account." + "api_key": "API-sleutel" }, "description": "U heeft de API-sleutel nodig van https://app.rach.io/. Selecteer 'Accountinstellingen en klik vervolgens op' GET API KEY '.", "title": "Maak verbinding met uw Rachio-apparaat" diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 27365271014..542ff285261 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -7,7 +7,7 @@ from pytz import timezone import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -26,7 +26,6 @@ from homeassistant.const import ( HTTP_OK, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -95,7 +94,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([RadarrSensor(hass, config, sensor) for sensor in conditions], True) -class RadarrSensor(Entity): +class RadarrSensor(SensorEntity): """Implementation of the Radarr sensor.""" def __init__(self, hass, conf, sensor_type): @@ -144,7 +143,7 @@ class RadarrSensor(Entity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" attributes = {} if self.type == "upcoming": diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index f09ef95170f..aad6bf3989e 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -181,7 +181,7 @@ class RadioThermostat(ClimateEntity): return PRECISION_HALVES @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" return {ATTR_FAN_ACTION: self._fstate} @@ -372,6 +372,6 @@ class RadioThermostat(ClimateEntity): self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode] else: _LOGGER.error( - "preset_mode %s not in PRESET_MODES", + "Preset_mode %s not in PRESET_MODES", preset_mode, ) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 83c358c480b..d8334470b60 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv CONF_ZONES = "zones" -SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN] +PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN] _LOGGER = logging.getLogger(__name__) @@ -80,7 +80,7 @@ def _setup_controller(hass, controller_config, config): return False hass.data[DATA_RAINBIRD].append(controller) _LOGGER.debug("Rain Bird Controller %d set to: %s", position, server) - for platform in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: discovery.load_platform( hass, platform, diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 501566de682..2c542dc12a9 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -3,7 +3,7 @@ import logging from pyrainbird import RainbirdController -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from . import ( DATA_RAINBIRD, @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class RainBirdSensor(Entity): +class RainBirdSensor(SensorEntity): """A sensor implementation for Rain Bird device.""" def __init__(self, controller: RainbirdController, sensor_type): diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index bccd4d2986c..7acb9740616 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -78,7 +78,7 @@ class RainBirdSwitch(SwitchEntity): self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone} @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return state attributes.""" return self._attributes diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 5955ef67168..51e9f5de5ce 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -161,12 +161,7 @@ class RainCloudEntity(Entity): self.schedule_update_ha_state(True) @property - def unit_of_measurement(self): - """Return the units of measurement.""" - return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) - - @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION, "identifier": self.data.serial} diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index b819b51365e..ee8f68734ad 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -3,12 +3,18 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from . import DATA_RAINCLOUD, ICON_MAP, SENSORS, RainCloudEntity +from . import ( + DATA_RAINCLOUD, + ICON_MAP, + SENSORS, + UNIT_OF_MEASUREMENT_MAP, + RainCloudEntity, +) _LOGGER = logging.getLogger(__name__) @@ -38,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class RainCloudSensor(RainCloudEntity): +class RainCloudSensor(RainCloudEntity, SensorEntity): """A sensor implementation for raincloud device.""" @property @@ -46,6 +52,11 @@ class RainCloudSensor(RainCloudEntity): """Return the state of the sensor.""" return self._state + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) + def update(self): """Get the latest data and updates the states.""" _LOGGER.debug("Updating RainCloud sensor: %s", self._name) diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index d6733412cac..d15f5c7c047 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -83,7 +83,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): self._state = self.data.auto_watering @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 99751e63f5b..d333f9437f1 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -7,14 +7,13 @@ from requests.exceptions import ConnectionError as ConnectError, HTTPError, Time from uEagle import Eagle as LegacyReader import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_IP_ADDRESS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle CONF_CLOUD_ID = "cloud_id" @@ -56,14 +55,18 @@ def hwtest(cloud_id, install_code, ip_address): response = reader.get_network_info() # Branch to test if target is Legacy Model - if "NetworkInfo" in response: - if response["NetworkInfo"].get("ModelId", None) == "Z109-EAGLE": - return reader + if ( + "NetworkInfo" in response + and response["NetworkInfo"].get("ModelId", None) == "Z109-EAGLE" + ): + return reader # Branch to test if target is Eagle-200 Model - if "Response" in response: - if response["Response"].get("Command", None) == "get_network_info": - return EagleReader(ip_address, cloud_id, install_code) + if ( + "Response" in response + and response["Response"].get("Command", None) == "get_network_info" + ): + return EagleReader(ip_address, cloud_id, install_code) # Catch-all if hardware ID tests fail raise ValueError("Couldn't determine device model.") @@ -95,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class EagleSensor(Entity): +class EagleSensor(SensorEntity): """Implementation of the Rainforest Eagle-200 sensor.""" def __init__(self, eagle_data, sensor_type, name, unit): @@ -160,7 +163,7 @@ class EagleData: return state -class LeagleReader(LegacyReader): +class LeagleReader(LegacyReader, SensorEntity): """Wraps uEagle to make it behave like eagle_reader, offering update().""" def update(self): diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 98fbdbcf401..e71e8a1f6d2 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -155,9 +155,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*controller_init_tasks) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) @@ -170,8 +170,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -225,7 +225,7 @@ class RainMachineEntity(CoordinatorEntity): } @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 80540491ee7..e076a105576 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -8,12 +8,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import ( # pylint: disable=unused-import - CONF_ZONE_RUN_TIME, - DEFAULT_PORT, - DEFAULT_ZONE_RUN, - DOMAIN, -) +from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN DATA_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 4533397fb54..20912809cb1 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -4,6 +4,7 @@ from typing import Callable from regenmaschine.controller import Controller +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback @@ -108,7 +109,7 @@ async def async_setup_entry( ) -class RainMachineSensor(RainMachineEntity): +class RainMachineSensor(RainMachineEntity, SensorEntity): """Define a general RainMachine sensor.""" def __init__( diff --git a/homeassistant/components/rainmachine/translations/he.json b/homeassistant/components/rainmachine/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/rainmachine/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index 44e24519ca2..48718980e2e 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { "user": { "data": { @@ -16,7 +22,8 @@ "init": { "data": { "zone_run_time": "Alap\u00e9rtelmezett z\u00f3nafut\u00e1si id\u0151 (m\u00e1sodpercben)" - } + }, + "title": "RainMachine konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json new file mode 100644 index 00000000000..482ffb75278 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "ip_address": "Nama Host atau Alamat IP", + "password": "Kata Sandi", + "port": "Port" + }, + "title": "Isi informasi Anda" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "Waktu berjalan zona default (dalam detik)" + }, + "title": "Konfigurasikan RainMachine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/ko.json b/homeassistant/components/rainmachine/translations/ko.json index 08ccfd1f5b9..0e38f4c4dfa 100644 --- a/homeassistant/components/rainmachine/translations/ko.json +++ b/homeassistant/components/rainmachine/translations/ko.json @@ -16,5 +16,15 @@ "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825\ud558\uae30" } } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "\uae30\ubcf8 \uc9c0\uc5ed \uc2e4\ud589 \uc2dc\uac04 (\ucd08)" + }, + "title": "RainMachine \uad6c\uc131\ud558\uae30" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 119e4c641af..8b767ced6c0 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -20,6 +20,9 @@ "options": { "step": { "init": { + "data": { + "zone_run_time": "Standaardlooptijd van de zone (in seconden)" + }, "title": "Configureer RainMachine" } } diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 58d996bc6ec..6465b828be1 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -3,7 +3,7 @@ from random import randrange import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MAXIMUM, CONF_MINIMUM, @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTR_MAXIMUM = "maximum" ATTR_MINIMUM = "minimum" @@ -42,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([RandomSensor(name, minimum, maximum, unit)], True) -class RandomSensor(Entity): +class RandomSensor(SensorEntity): """Representation of a Random number sensor.""" def __init__(self, name, minimum, maximum, unit_of_measurement): @@ -74,7 +73,7 @@ class RandomSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the sensor.""" return {ATTR_MAXIMUM: self._maximum, ATTR_MINIMUM: self._minimum} diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 0600d73d8a1..2e6f780c749 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,14 +1,14 @@ """The ReCollect Waste integration.""" +from __future__ import annotations + import asyncio from datetime import date, timedelta -from typing import List from aiorecollect.client import Client, PickupEvent from aiorecollect.errors import RecollectError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PLACE_ID], entry.data[CONF_SERVICE_ID], session=session ) - async def async_get_pickup_events() -> List[PickupEvent]: + async def async_get_pickup_events() -> list[PickupEvent]: """Get the next pickup.""" try: return await client.async_get_pickup_events( @@ -54,16 +54,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_get_pickup_events, ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( @@ -83,8 +80,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index 8e208f57cc6..62b42e2bddf 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -1,5 +1,5 @@ """Config flow for ReCollect Waste integration.""" -from typing import Optional +from __future__ import annotations from aiorecollect.client import Client from aiorecollect.errors import RecollectError @@ -10,12 +10,7 @@ from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import ( # pylint:disable=unused-import - CONF_PLACE_ID, - CONF_SERVICE_ID, - DOMAIN, - LOGGER, -) +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER DATA_SCHEMA = vol.Schema( {vol.Required(CONF_PLACE_ID): str, vol.Required(CONF_SERVICE_ID): str} @@ -83,7 +78,7 @@ class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): """Initialize.""" self._entry = entry - async def async_step_init(self, user_input: Optional[dict] = None): + async def async_step_init(self, user_input: dict | None = None): """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 66ced51b77f..1c3dabc2c87 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,10 +1,12 @@ """Support for ReCollect Waste sensors.""" -from typing import Callable, List +from __future__ import annotations + +from typing import Callable from aiorecollect.client import PickupType import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +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.core import HomeAssistant, callback @@ -36,8 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @callback def async_get_pickup_type_names( - entry: ConfigEntry, pickup_types: List[PickupType] -) -> List[str]: + entry: ConfigEntry, pickup_types: list[PickupType] +) -> list[str]: """Return proper pickup type names from their associated objects.""" return [ t.friendly_name @@ -55,8 +57,8 @@ async def async_setup_platform( ): """Import Recollect Waste configuration from YAML.""" LOGGER.warning( - "Loading ReCollect Waste via platform setup is deprecated. " - "Please remove it from your configuration." + "Loading ReCollect Waste via platform setup is deprecated; " + "Please remove it from your configuration" ) hass.async_create_task( hass.config_entries.flow.async_init( @@ -75,7 +77,7 @@ async def async_setup_entry( async_add_entities([ReCollectWasteSensor(coordinator, entry)]) -class ReCollectWasteSensor(CoordinatorEntity): +class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: @@ -86,7 +88,7 @@ class ReCollectWasteSensor(CoordinatorEntity): self._state = None @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/recollect_waste/translations/hu.json b/homeassistant/components/recollect_waste/translations/hu.json index 112c8cb8385..3222f50be02 100644 --- a/homeassistant/components/recollect_waste/translations/hu.json +++ b/homeassistant/components/recollect_waste/translations/hu.json @@ -14,5 +14,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "Recollect Waste konfigur\u00e1l\u00e1sa" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/id.json b/homeassistant/components/recollect_waste/translations/id.json new file mode 100644 index 00000000000..1c0656810dd --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_place_or_service_id": "Place atau Service ID tidak valid" + }, + "step": { + "user": { + "data": { + "place_id": "Place ID", + "service_id": "Service ID" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Gunakan nama alias untuk jenis pengambilan (jika memungkinkan)" + }, + "title": "Konfigurasikan Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/ko.json b/homeassistant/components/recollect_waste/translations/ko.json index 17dee71d640..422843c67f2 100644 --- a/homeassistant/components/recollect_waste/translations/ko.json +++ b/homeassistant/components/recollect_waste/translations/ko.json @@ -2,6 +2,27 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_place_or_service_id": "\uc7a5\uc18c \ub610\ub294 \uc11c\ube44\uc2a4 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "place_id": "\uc7a5\uc18c ID", + "service_id": "\uc11c\ube44\uc2a4 ID" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\uc218\uac70 \uc720\ud615\uc5d0 \uce5c\uc219\ud55c \uc774\ub984\uc744 \uc0ac\uc6a9\ud558\uae30 (\uac00\ub2a5\ud55c \uacbd\uc6b0)" + }, + "title": "Recollect Waste \uad6c\uc131\ud558\uae30" + } } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/nl.json b/homeassistant/components/recollect_waste/translations/nl.json index 6ce4a8f8a9f..eec63605267 100644 --- a/homeassistant/components/recollect_waste/translations/nl.json +++ b/homeassistant/components/recollect_waste/translations/nl.json @@ -18,6 +18,9 @@ "options": { "step": { "init": { + "data": { + "friendly_name": "Gebruik vriendelijke namen voor afhaaltypes (indien mogelijk)" + }, "title": "Configureer Recollect Waste" } } diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 3935aa97eb8..f93d965a4b9 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1,6 +1,7 @@ """Support for recording details.""" +from __future__ import annotations + import asyncio -from collections import namedtuple import concurrent.futures from datetime import datetime import logging @@ -8,7 +9,7 @@ import queue import sqlite3 import threading import time -from typing import Any, Callable, List, Optional +from typing import Any, Callable, NamedTuple from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select from sqlalchemy.orm import scoped_session, sessionmaker @@ -53,11 +54,13 @@ SERVICE_DISABLE = "disable" ATTR_KEEP_DAYS = "keep_days" ATTR_REPACK = "repack" +ATTR_APPLY_FILTER = "apply_filter" SERVICE_PURGE_SCHEMA = vol.Schema( { vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, vol.Optional(ATTR_REPACK, default=False): cv.boolean, + vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, } ) SERVICE_ENABLE_SCHEMA = vol.Schema({}) @@ -124,7 +127,7 @@ CONFIG_SCHEMA = vol.Schema( ) -def run_information(hass, point_in_time: Optional[datetime] = None): +def run_information(hass, point_in_time: datetime | None = None): """Return information about current run. There is also the run that covers point_in_time. @@ -137,7 +140,7 @@ def run_information(hass, point_in_time: Optional[datetime] = None): return run_information_with_session(session, point_in_time) -def run_information_from_instance(hass, point_in_time: Optional[datetime] = None): +def run_information_from_instance(hass, point_in_time: datetime | None = None): """Return information about current run from the existing instance. Does not query the database for older runs. @@ -148,7 +151,7 @@ def run_information_from_instance(hass, point_in_time: Optional[datetime] = None return ins.run_info -def run_information_with_session(session, point_in_time: Optional[datetime] = None): +def run_information_with_session(session, point_in_time: datetime | None = None): """Return information about current run from the database.""" recorder_runs = RecorderRuns @@ -223,7 +226,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return await instance.async_db_ready -PurgeTask = namedtuple("PurgeTask", ["keep_days", "repack"]) +class PurgeTask(NamedTuple): + """Object to store information about purge task.""" + + keep_days: int + repack: bool + apply_filter: bool class WaitTask: @@ -243,7 +251,7 @@ class Recorder(threading.Thread): db_max_retries: int, db_retry_wait: int, entity_filter: Callable[[str], bool], - exclude_t: List[str], + exclude_t: list[str], db_integrity_check: bool, ) -> None: """Initialize the recorder.""" @@ -296,18 +304,15 @@ class Recorder(threading.Thread): return False entity_id = event.data.get(ATTR_ENTITY_ID) - if entity_id is not None: - if not self.entity_filter(entity_id): - return False - - return True + return bool(entity_id is None or self.entity_filter(entity_id)) def do_adhoc_purge(self, **kwargs): """Trigger an adhoc purge retaining keep_days worth of data.""" keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days) repack = kwargs.get(ATTR_REPACK) + apply_filter = kwargs.get(ATTR_APPLY_FILTER) - self.queue.put(PurgeTask(keep_days, repack)) + self.queue.put(PurgeTask(keep_days, repack, apply_filter)) def run(self): """Start processing events to save.""" @@ -361,7 +366,9 @@ class Recorder(threading.Thread): @callback def async_purge(now): """Trigger the purge.""" - self.queue.put(PurgeTask(self.keep_days, repack=False)) + self.queue.put( + PurgeTask(self.keep_days, repack=False, apply_filter=False) + ) # Purge every night at 4:12am self.hass.helpers.event.track_time_change( @@ -391,7 +398,7 @@ class Recorder(threading.Thread): migration.migrate_schema(self) self._setup_run() except Exception as err: # pylint: disable=broad-except - _LOGGER.error( + _LOGGER.exception( "Error during connection setup to %s: %s (retrying in %s seconds)", self.db_url, err, @@ -422,8 +429,12 @@ class Recorder(threading.Thread): """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): - self.queue.put(PurgeTask(event.keep_days, event.repack)) + 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) + ) return if isinstance(event, WaitTask): self._queue_watch.set() diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index a2b5ffc6f2a..026628a32df 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -5,3 +5,6 @@ SQLITE_URL_PREFIX = "sqlite://" DOMAIN = "recorder" CONF_DB_INTEGRITY_CHECK = "db_integrity_check" + +# The maximum number of rows (events) we purge in one delete statement +MAX_ROWS_TO_PURGE = 1000 diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index aeb62cc111d..5ab2d909172 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -204,6 +204,44 @@ def _add_columns(engine, table_name, columns_def): ) +def _modify_columns(engine, table_name, columns_def): + """Modify columns in a table.""" + _LOGGER.warning( + "Modifying columns %s in table %s. Note: this can take several " + "minutes on large databases and slow computers. Please " + "be patient!", + ", ".join(column.split(" ")[0] for column in columns_def), + table_name, + ) + columns_def = [f"MODIFY {col_def}" for col_def in columns_def] + + try: + engine.execute( + text( + "ALTER TABLE {table} {columns_def}".format( + table=table_name, columns_def=", ".join(columns_def) + ) + ) + ) + return + except (InternalError, OperationalError): + _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") + + for column_def in columns_def: + try: + engine.execute( + text( + "ALTER TABLE {table} {column_def}".format( + table=table_name, column_def=column_def + ) + ) + ) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not modify column %s in table %s", column_def, table_name + ) + + def _update_states_table_with_foreign_key_options(engine): """Add the options to foreign key constraints.""" inspector = reflection.Inspector.from_engine(engine) @@ -321,6 +359,24 @@ def _apply_update(engine, new_version, old_version): elif new_version == 11: _create_index(engine, "states", "ix_states_old_state_id") _update_states_table_with_foreign_key_options(engine) + elif new_version == 12: + if engine.dialect.name == "mysql": + _modify_columns(engine, "events", ["event_data LONGTEXT"]) + _modify_columns(engine, "states", ["attributes LONGTEXT"]) + elif new_version == 13: + if engine.dialect.name == "mysql": + _modify_columns( + engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"] + ) + _modify_columns( + engine, + "states", + [ + "last_changed DATETIME(6)", + "last_updated DATETIME(6)", + "created DATETIME(6)", + ], + ) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 9481e954bde..a547f315133 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -13,6 +13,7 @@ from sqlalchemy import ( Text, distinct, ) +from sqlalchemy.dialects import mysql from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.orm.session import Session @@ -25,7 +26,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 11 +SCHEMA_VERSION = 13 _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,10 @@ TABLE_SCHEMA_CHANGES = "schema_changes" ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) + class Events(Base): # type: ignore """Event history data.""" @@ -49,10 +54,10 @@ class Events(Base): # type: ignore __tablename__ = TABLE_EVENTS event_id = Column(Integer, primary_key=True) event_type = Column(String(32)) - event_data = Column(Text) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) - time_fired = Column(DateTime(timezone=True), index=True) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) context_id = Column(String(36), index=True) context_user_id = Column(String(36), index=True) context_parent_id = Column(String(36), index=True) @@ -63,6 +68,15 @@ class Events(Base): # type: ignore Index("ix_events_event_type_time_fired", "event_type", "time_fired"), ) + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + @staticmethod def from_event(event, event_data=None): """Create an event database object from a native event.""" @@ -109,15 +123,15 @@ class States(Base): # type: ignore domain = Column(String(64)) entity_id = Column(String(255)) state = Column(String(255)) - attributes = Column(Text) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) event_id = Column( Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) - last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow) - last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column( - Integer, ForeignKey("states.state_id", ondelete="SET NULL"), index=True + Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True ) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) @@ -128,6 +142,17 @@ class States(Base): # type: ignore Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), ) + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + @staticmethod def from_event(event): """Create object from a state_changed event.""" @@ -184,6 +209,19 @@ class RecorderRuns(Base): # type: ignore __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + def entity_ids(self, point_in_time=None): """Return the entity ids that existed in this run. @@ -218,6 +256,15 @@ class SchemaChanges(Base): # type: ignore schema_version = Column(Integer) changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + def process_timestamp(ts): """Process a timestamp into datetime object.""" diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 43e84785f7d..ef626a744c4 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -1,88 +1,59 @@ """Purge old data helper.""" -from datetime import timedelta +from __future__ import annotations + +from datetime import datetime, timedelta import logging import time +from typing import TYPE_CHECKING from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.orm.session import Session +from sqlalchemy.sql.expression import distinct import homeassistant.util.dt as dt_util +from .const import MAX_ROWS_TO_PURGE from .models import Events, RecorderRuns, States -from .util import execute, session_scope +from .repack import repack_database +from .util import session_scope + +if TYPE_CHECKING: + from . import Recorder _LOGGER = logging.getLogger(__name__) -def purge_old_data(instance, purge_days: int, repack: bool) -> bool: +def purge_old_data( + instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False +) -> bool: """Purge events and states older than purge_days ago. Cleans up an timeframe of an hour, based on the oldest record. """ purge_before = dt_util.utcnow() - timedelta(days=purge_days) - _LOGGER.debug("Purging states and events before target %s", purge_before) - + _LOGGER.debug( + "Purging states and events before target %s", + purge_before.isoformat(sep=" ", timespec="seconds"), + ) try: - with session_scope(session=instance.get_session()) as session: - # Purge a max of 1 hour, based on the oldest states or events record - batch_purge_before = purge_before - - query = session.query(States).order_by(States.last_updated.asc()).limit(1) - states = execute(query, to_native=True, validate_entity_ids=False) - if states: - batch_purge_before = min( - batch_purge_before, - states[0].last_updated + timedelta(hours=1), - ) - - query = session.query(Events).order_by(Events.time_fired.asc()).limit(1) - events = execute(query, to_native=True) - if events: - batch_purge_before = min( - batch_purge_before, - events[0].time_fired + timedelta(hours=1), - ) - - _LOGGER.debug("Purging states and events before %s", batch_purge_before) - - deleted_rows = ( - session.query(States) - .filter(States.last_updated < batch_purge_before) - .delete(synchronize_session=False) - ) - _LOGGER.debug("Deleted %s states", deleted_rows) - - deleted_rows = ( - session.query(Events) - .filter(Events.time_fired < batch_purge_before) - .delete(synchronize_session=False) - ) - _LOGGER.debug("Deleted %s events", deleted_rows) - - # If states or events purging isn't processing the purge_before yet, - # return false, as we are not done yet. - if batch_purge_before != purge_before: + with session_scope(session=instance.get_session()) as session: # type: ignore + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + event_ids = _select_event_ids_to_purge(session, purge_before) + state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) + if state_ids: + _purge_state_ids(session, state_ids) + if event_ids: + _purge_event_ids(session, event_ids) + # If states or events purging isn't processing the purge_before yet, + # return false, as we are not done yet. _LOGGER.debug("Purging hasn't fully completed yet") return False - - # Recorder runs is small, no need to batch run it - deleted_rows = ( - session.query(RecorderRuns) - .filter(RecorderRuns.start < purge_before) - .filter(RecorderRuns.run_id != instance.run_info.run_id) - .delete(synchronize_session=False) - ) - _LOGGER.debug("Deleted %s recorder_runs", deleted_rows) - + if apply_filter and _purge_filtered_data(instance, session) is False: + _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") + return False + _purge_old_recorder_runs(instance, session, purge_before) if repack: - # Execute sqlite or postgresql vacuum command to free up space on disk - if instance.engine.driver in ("pysqlite", "postgresql"): - _LOGGER.debug("Vacuuming SQL DB to free space") - instance.engine.execute("VACUUM") - # Optimize mysql / mariadb tables to free up space on disk - elif instance.engine.driver in ("mysqldb", "pymysql"): - _LOGGER.debug("Optimizing SQL DB to free space") - instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs") - + repack_database(instance) except OperationalError as err: # Retry when one of the following MySQL errors occurred: # 1205: Lock wait timeout exceeded; try restarting transaction @@ -101,3 +72,144 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: except SQLAlchemyError as err: _LOGGER.warning("Error purging history: %s", err) return True + + +def _select_event_ids_to_purge(session: Session, purge_before: datetime) -> list[int]: + """Return a list of event ids to purge.""" + events = ( + session.query(Events.event_id) + .filter(Events.time_fired < purge_before) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) + _LOGGER.debug("Selected %s event ids to remove", len(events)) + return [event.event_id for event in events] + + +def _select_state_ids_to_purge( + session: Session, purge_before: datetime, event_ids: list[int] +) -> list[int]: + """Return a list of state ids to purge.""" + if not event_ids: + return [] + states = ( + session.query(States.state_id) + .filter(States.last_updated < purge_before) + .filter(States.event_id.in_(event_ids)) + .all() + ) + _LOGGER.debug("Selected %s state ids to remove", len(states)) + return [state.state_id for state in states] + + +def _purge_state_ids(session: Session, state_ids: list[int]) -> None: + """Disconnect states and delete by state id.""" + + # Update old_state_id to NULL before deleting to ensure + # the delete does not fail due to a foreign key constraint + # since some databases (MSSQL) cannot do the ON DELETE SET NULL + # for us. + disconnected_rows = ( + session.query(States) + .filter(States.old_state_id.in_(state_ids)) + .update({"old_state_id": None}, synchronize_session=False) + ) + _LOGGER.debug("Updated %s states to remove old_state_id", disconnected_rows) + + deleted_rows = ( + session.query(States) + .filter(States.state_id.in_(state_ids)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s states", deleted_rows) + + +def _purge_event_ids(session: Session, event_ids: list[int]) -> None: + """Delete by event id.""" + deleted_rows = ( + session.query(Events) + .filter(Events.event_id.in_(event_ids)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s events", deleted_rows) + + +def _purge_old_recorder_runs( + instance: Recorder, session: Session, purge_before: datetime +) -> None: + """Purge all old recorder runs.""" + # Recorder runs is small, no need to batch run it + deleted_rows = ( + session.query(RecorderRuns) + .filter(RecorderRuns.start < purge_before) + .filter(RecorderRuns.run_id != instance.run_info.run_id) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s recorder_runs", deleted_rows) + + +def _purge_filtered_data(instance: Recorder, session: Session) -> bool: + """Remove filtered states and events that shouldn't be in the database.""" + _LOGGER.debug("Cleanup filtered data") + + # Check if excluded entity_ids are in database + excluded_entity_ids: list[str] = [ + entity_id + for (entity_id,) in session.query(distinct(States.entity_id)).all() + if not instance.entity_filter(entity_id) + ] + if len(excluded_entity_ids) > 0: + _purge_filtered_states(session, excluded_entity_ids) + return False + + # Check if excluded event_types are in database + excluded_event_types: list[str] = [ + event_type + for (event_type,) in session.query(distinct(Events.event_type)).all() + if event_type in instance.exclude_t + ] + if len(excluded_event_types) > 0: + _purge_filtered_events(session, excluded_event_types) + return False + + return True + + +def _purge_filtered_states(session: Session, excluded_entity_ids: list[str]) -> None: + """Remove filtered states and linked events.""" + state_ids: list[int] + event_ids: list[int | None] + state_ids, event_ids = zip( + *( + session.query(States.state_id, States.event_id) + .filter(States.entity_id.in_(excluded_entity_ids)) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) + ) + event_ids = [id_ for id_ in event_ids if id_ is not None] + _LOGGER.debug( + "Selected %s state_ids to remove that should be filtered", len(state_ids) + ) + _purge_state_ids(session, state_ids) + _purge_event_ids(session, event_ids) # type: ignore # type of event_ids already narrowed to 'list[int]' + + +def _purge_filtered_events(session: Session, excluded_event_types: list[str]) -> None: + """Remove filtered events and linked states.""" + events: list[Events] = ( + session.query(Events.event_id) + .filter(Events.event_type.in_(excluded_event_types)) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) + event_ids: list[int] = [event.event_id for event in events] + _LOGGER.debug( + "Selected %s event_ids to remove that should be filtered", len(event_ids) + ) + states: list[States] = ( + session.query(States.state_id).filter(States.event_id.in_(event_ids)).all() + ) + state_ids: list[int] = [state.state_id for state in states] + _purge_state_ids(session, state_ids) + _purge_event_ids(session, event_ids) diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py new file mode 100644 index 00000000000..68d7d5954c9 --- /dev/null +++ b/homeassistant/components/recorder/repack.py @@ -0,0 +1,35 @@ +"""Purge repack helper.""" +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import Recorder + +_LOGGER = logging.getLogger(__name__) + + +def repack_database(instance: Recorder) -> None: + """Repack based on engine type.""" + + # Execute sqlite command to free up space on disk + if instance.engine.dialect.name == "sqlite": + _LOGGER.debug("Vacuuming SQL DB to free space") + instance.engine.execute("VACUUM") + return + + # Execute postgresql vacuum command to free up space on disk + if instance.engine.dialect.name == "postgresql": + _LOGGER.debug("Vacuuming SQL DB to free space") + with instance.engine.connect().execution_options( + isolation_level="AUTOCOMMIT" + ) as conn: + conn.execute("VACUUM") + return + + # Optimize mysql / mariadb tables to free up space on disk + if instance.engine.dialect.name == "mysql": + _LOGGER.debug("Optimizing SQL DB to free space") + instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs") + return diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 2be5b0e095e..2c4f35b5e7a 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -25,6 +25,14 @@ purge: selector: boolean: + apply_filter: + name: Apply filter + description: Apply entity_id and event_type filter in addition to time based purge. + example: true + default: false + selector: + boolean: + disable: description: Stop the recording of events and state changes diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b945386de82..c17fb33d365 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -1,4 +1,7 @@ """SQLAlchemy util functions.""" +from __future__ import annotations + +from collections.abc import Generator from contextlib import contextmanager from datetime import timedelta import logging @@ -6,7 +9,9 @@ import os import time from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.orm.session import Session +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, SQLITE_URL_PREFIX @@ -25,7 +30,9 @@ MAX_RESTART_TIME = timedelta(minutes=10) @contextmanager -def session_scope(*, hass=None, session=None): +def session_scope( + *, hass: HomeAssistantType | 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: session = hass.data[DATA_INSTANCE].get_session() @@ -168,7 +175,7 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: run_checks_on_open_db(dbpath, conn.cursor(), db_integrity_check) conn.close() except sqlite3.DatabaseError: - _LOGGER.exception("The database at %s is corrupt or malformed.", dbpath) + _LOGGER.exception("The database at %s is corrupt or malformed", dbpath) return False return True @@ -203,7 +210,7 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check): if not last_run_was_clean: _LOGGER.warning( - "The system could not validate that the sqlite3 database at %s was shutdown cleanly.", + "The system could not validate that the sqlite3 database at %s was shutdown cleanly", dbpath, ) diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 7a04fb6a8ae..a88de916009 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -5,7 +5,7 @@ import logging import praw import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ID, CONF_CLIENT_ID, @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -82,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class RedditSensor(Entity): +class RedditSensor(SensorEntity): """Representation of a Reddit sensor.""" def __init__(self, reddit, subreddit: str, limit: int, sort_by: str): @@ -105,7 +104,7 @@ class RedditSensor(Entity): return len(self._subreddit_data) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_SUBREDDIT: self._subreddit, diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 30d57a3d9dc..78b713c286c 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -4,6 +4,7 @@ Support for Rejseplanen information from rejseplanen.dk. For more info on the API see: https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API """ +from contextlib import suppress from datetime import datetime, timedelta import logging from operator import itemgetter @@ -11,10 +12,9 @@ from operator import itemgetter import rjpl import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -87,7 +87,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) -class RejseplanenTransportSensor(Entity): +class RejseplanenTransportSensor(SensorEntity): """Implementation of Rejseplanen transport sensor.""" def __init__(self, data, stop_id, route, direction, name): @@ -110,7 +110,7 @@ class RejseplanenTransportSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not self._times: return {ATTR_STOP_ID: self._stop_id, ATTR_ATTRIBUTION: ATTRIBUTION} @@ -148,10 +148,8 @@ class RejseplanenTransportSensor(Entity): if not self._times: self._state = None else: - try: + with suppress(TypeError): self._state = self._times[0][ATTR_DUE_IN] - except TypeError: - pass class PublicTransportData: diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 44a318988b2..ecde6f67b67 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -1,13 +1,16 @@ """Support to interface with universal remote control devices.""" +from __future__ import annotations + from datetime import timedelta import functools as ft import logging -from typing import Any, Iterable, cast +from typing import Any, Iterable, cast, final import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_COMMAND, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -29,7 +32,8 @@ from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" -ATTR_COMMAND = "command" +ATTR_ACTIVITY_LIST = "activity_list" +ATTR_CURRENT_ACTIVITY = "current_activity" ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE = "device" ATTR_NUM_REPEATS = "num_repeats" @@ -56,6 +60,7 @@ DEFAULT_HOLD_SECS = 0 SUPPORT_LEARN_COMMAND = 1 SUPPORT_DELETE_COMMAND = 2 +SUPPORT_ACTIVITY = 4 REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} @@ -136,20 +141,41 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo class RemoteEntity(ToggleEntity): - """Representation of a remote.""" + """Base class for remote entities.""" @property def supported_features(self) -> int: """Flag supported features.""" return 0 + @property + def current_activity(self) -> str | None: + """Active activity.""" + return None + + @property + def activity_list(self) -> list[str] | None: + """List of available activities.""" + return None + + @final + @property + def state_attributes(self) -> dict[str, Any] | None: + """Return optional state attributes.""" + if not self.supported_features & SUPPORT_ACTIVITY: + return None + + return { + ATTR_ACTIVITY_LIST: self.activity_list, + ATTR_CURRENT_ACTIVITY: self.current_activity, + } + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send commands to a device.""" raise NotImplementedError() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send commands to a device.""" - assert self.hass is not None await self.hass.async_add_executor_job( ft.partial(self.send_command, command, **kwargs) ) @@ -160,7 +186,6 @@ class RemoteEntity(ToggleEntity): async def async_learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" - assert self.hass is not None await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs)) def delete_command(self, **kwargs: Any) -> None: @@ -169,7 +194,6 @@ class RemoteEntity(ToggleEntity): async def async_delete_command(self, **kwargs: Any) -> None: """Delete commands from the database.""" - assert self.hass is not None await self.hass.async_add_executor_job( ft.partial(self.delete_command, **kwargs) ) diff --git a/homeassistant/components/remote/device_action.py b/homeassistant/components/remote/device_action.py index aa819f3eb46..aa34eb33224 100644 --- a/homeassistant/components/remote/device_action.py +++ b/homeassistant/components/remote/device_action.py @@ -1,5 +1,5 @@ """Provides device actions for remotes.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -25,6 +25,6 @@ async def async_call_action_from_config( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions.""" return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py index 06c7bec89d4..ed200fd5579 100644 --- a/homeassistant/components/remote/device_condition.py +++ b/homeassistant/components/remote/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for remotes.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ def async_condition_from_config( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py index 5919e8c61ba..d8437604f6d 100644 --- a/homeassistant/components/remote/device_trigger.py +++ b/homeassistant/components/remote/device_trigger.py @@ -1,5 +1,5 @@ """Provides device triggers for remotes.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ async def async_attach_trigger( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index 4e1f426c57b..b42a0bdc611 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Remote state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -24,8 +26,8 @@ async def _async_reproduce_state( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -60,8 +62,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Remote states.""" await asyncio.gather( diff --git a/homeassistant/components/remote/translations/hu.json b/homeassistant/components/remote/translations/hu.json index fa0bf3fee90..39ce5f17a12 100644 --- a/homeassistant/components/remote/translations/hu.json +++ b/homeassistant/components/remote/translations/hu.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "{entity_name} be/kikapcsol\u00e1sa", + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + }, "state": { "_": { "off": "Ki", diff --git a/homeassistant/components/remote/translations/id.json b/homeassistant/components/remote/translations/id.json index e824cafff4e..09552be40d4 100644 --- a/homeassistant/components/remote/translations/id.json +++ b/homeassistant/components/remote/translations/id.json @@ -1,8 +1,23 @@ { + "device_automation": { + "action_type": { + "toggle": "Nyala/matikan {entity_name}", + "turn_off": "Matikan {entity_name}", + "turn_on": "Nyalakan {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala" + }, + "trigger_type": { + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan" + } + }, "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Daring" diff --git a/homeassistant/components/remote/translations/ko.json b/homeassistant/components/remote/translations/ko.json index bd055e21f5b..a89c4b36f35 100644 --- a/homeassistant/components/remote/translations/ko.json +++ b/homeassistant/components/remote/translations/ko.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "{entity_name} \ud1a0\uae00", - "turn_off": "{entity_name} \ub044\uae30", - "turn_on": "{entity_name} \ucf1c\uae30" + "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30", + "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", + "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774 \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774 \ucf1c\uc838 \uc788\uc73c\uba74" + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uaebc\uc9d0", - "turned_on": "{entity_name} \ucf1c\uc9d0" + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/remote/translations/zh-Hans.json b/homeassistant/components/remote/translations/zh-Hans.json index ba1344fbb74..f6c509d4a08 100644 --- a/homeassistant/components/remote/translations/zh-Hans.json +++ b/homeassistant/components/remote/translations/zh-Hans.json @@ -1,7 +1,9 @@ { "device_automation": { "action_type": { - "turn_off": "\u5173\u95ed {entity_name}" + "toggle": "\u5207\u6362 {entity_name} \u5f00\u5173", + "turn_off": "\u5173\u95ed {entity_name}", + "turn_on": "\u6253\u5f00 {entity_name}" }, "condition_type": { "is_off": "{entity_name} \u5df2\u5173\u95ed", diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index e342b2d341e..77a3c51e9cf 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -3,10 +3,10 @@ from datetime import datetime import logging import time +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class RepetierSensor(Entity): +class RepetierSensor(SensorEntity): """Class to create and populate a Repetier Sensor.""" def __init__(self, api, temp_id, name, printer_id, sensor_type): @@ -66,7 +66,7 @@ class RepetierSensor(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return sensor attributes.""" return self._attributes diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index ebeddcfd7c7..8b9390bb1c9 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -33,9 +33,9 @@ from homeassistant.helpers.entity_component import ( from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_IDX +from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX from .data import RestData -from .schema import CONFIG_SCHEMA # noqa:F401 pylint: disable=unused-import +from .schema import CONFIG_SCHEMA # noqa: F401 _LOGGER = logging.getLogger(__name__) @@ -67,7 +67,7 @@ async def async_setup(hass: HomeAssistant, config: dict): @callback def _async_setup_shared_data(hass: HomeAssistant): """Create shared data for platform config and rest coordinators.""" - hass.data[DOMAIN] = {platform: {} for platform in COORDINATOR_AWARE_PLATFORMS} + hass.data[DOMAIN] = {key: [] for key in [REST_DATA, *COORDINATOR_AWARE_PLATFORMS]} async def _async_process_config(hass, config) -> bool: @@ -81,18 +81,17 @@ async def _async_process_config(hass, config) -> bool: scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) resource_template = conf.get(CONF_RESOURCE_TEMPLATE) rest = create_rest_data_from_config(hass, conf) - coordinator = _wrap_rest_in_coordinator( - hass, rest, resource_template, scan_interval - ) + coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval) refresh_tasks.append(coordinator.async_refresh()) - hass.data[DOMAIN][rest_idx] = {REST: rest, COORDINATOR: coordinator} + hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) for platform_domain in COORDINATOR_AWARE_PLATFORMS: if platform_domain not in conf: continue - for platform_idx, platform_conf in enumerate(conf[platform_domain]): - hass.data[DOMAIN][platform_domain][platform_idx] = platform_conf + for platform_conf in conf[platform_domain]: + hass.data[DOMAIN][platform_domain].append(platform_conf) + platform_idx = len(hass.data[DOMAIN][platform_domain]) - 1 load = discovery.async_load_platform( hass, @@ -114,7 +113,7 @@ async def _async_process_config(hass, config) -> bool: async def async_get_config_and_coordinator(hass, platform_domain, discovery_info): """Get the config and coordinator for the platform from discovery.""" - shared_data = hass.data[DOMAIN][discovery_info[REST_IDX]] + shared_data = hass.data[DOMAIN][REST_DATA][discovery_info[REST_IDX]] conf = hass.data[DOMAIN][platform_domain][discovery_info[PLATFORM_IDX]] coordinator = shared_data[COORDINATOR] rest = shared_data[REST] @@ -123,7 +122,7 @@ async def async_get_config_and_coordinator(hass, platform_domain, discovery_info return conf, coordinator, rest -def _wrap_rest_in_coordinator(hass, rest, resource_template, update_interval): +def _rest_coordinator(hass, rest, resource_template, update_interval): """Wrap a DataUpdateCoordinator around the rest object.""" if resource_template: diff --git a/homeassistant/components/rest/const.py b/homeassistant/components/rest/const.py index 31216b65968..5fd32d8fba7 100644 --- a/homeassistant/components/rest/const.py +++ b/homeassistant/components/rest/const.py @@ -17,4 +17,6 @@ PLATFORM_IDX = "platform_idx" COORDINATOR = "coordinator" REST = "rest" +REST_DATA = "rest_data" + METHODS = ["POST", "GET"] diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 0699d9dc07c..d303f7a57b3 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -7,7 +7,11 @@ from jsonpath import jsonpath import voluptuous as vol import xmltodict -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, @@ -81,7 +85,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class RestSensor(RestEntity): +class RestSensor(RestEntity, SensorEntity): """Implementation of a REST sensor.""" def __init__( @@ -119,7 +123,7 @@ class RestSensor(RestEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 68783c3426a..c78b0c6f944 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -28,6 +28,8 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity +from .utils import brightness_to_rflink + _LOGGER = logging.getLogger(__name__) ATTR_EVENT = "event" @@ -67,6 +69,7 @@ SERVICE_SEND_COMMAND = "send_command" SIGNAL_AVAILABILITY = "rflink_device_available" SIGNAL_HANDLE_EVENT = "rflink_handle_event_{}" +SIGNAL_EVENT = "rflink_event" TMP_ENTITY = "tmp.{}" @@ -140,6 +143,15 @@ async def async_setup(hass, config): ) ): _LOGGER.error("Failed Rflink command for %s", str(call.data)) + else: + async_dispatcher_send( + hass, + SIGNAL_EVENT, + { + EVENT_KEY_ID: call.data.get(CONF_DEVICE_ID), + EVENT_KEY_COMMAND: call.data.get(CONF_COMMAND), + }, + ) hass.services.async_register( DOMAIN, SERVICE_SEND_COMMAND, async_send_command, schema=SEND_COMMAND_SCHEMA @@ -236,7 +248,7 @@ async def async_setup(hass, config): # If HA is not stopping, initiate new connection if hass.state != CoreState.stopping: - _LOGGER.warning("disconnected from Rflink, reconnecting") + _LOGGER.warning("Disconnected from Rflink, reconnecting") hass.async_create_task(connect()) async def connect(): @@ -293,6 +305,7 @@ async def async_setup(hass, config): _LOGGER.info("Connected to Rflink") hass.async_create_task(connect()) + async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback) return True @@ -498,7 +511,7 @@ class RflinkCommand(RflinkDevice): elif command == "dim": # convert brightness to rflink dim level - cmd = str(int(args[0] / 17)) + cmd = str(brightness_to_rflink(args[0])) self._state = True elif command == "toggle": diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index fe74c979396..5a0d6766179 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -1,5 +1,6 @@ """Support for Rflink lights.""" import logging +import re import voluptuous as vol @@ -27,6 +28,7 @@ from . import ( EVENT_KEY_ID, SwitchableRflinkDevice, ) +from .utils import brightness_to_rflink, rflink_to_brightness _LOGGER = logging.getLogger(__name__) @@ -183,30 +185,39 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels - self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 + self._brightness = rflink_to_brightness( + brightness_to_rflink(kwargs[ATTR_BRIGHTNESS]) + ) # Turn on light at the requested dim level await self._async_handle_command("dim", self._brightness) + def _handle_event(self, event): + """Adjust state if Rflink picks up a remote command for this device.""" + self.cancel_queued_send_commands() + + command = event["command"] + if command in ["on", "allon"]: + self._state = True + elif command in ["off", "alloff"]: + self._state = False + # dimmable device accept 'set_level=(0-15)' commands + elif re.search("^set_level=(0?[0-9]|1[0-5])$", command, re.IGNORECASE): + self._brightness = rflink_to_brightness(int(command.split("=")[1])) + self._state = True + @property def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness - @property - def device_state_attributes(self): - """Return the device state attributes.""" - if self._brightness is None: - return {} - return {ATTR_BRIGHTNESS: self._brightness} - @property def supported_features(self): """Flag supported features.""" return SUPPORT_BRIGHTNESS -class HybridRflinkLight(SwitchableRflinkDevice, LightEntity): +class HybridRflinkLight(DimmableRflinkLight, LightEntity): """Rflink light device that sends out both dim and on/off commands. Used for protocols which support lights that are not exclusively on/off @@ -221,52 +232,14 @@ class HybridRflinkLight(SwitchableRflinkDevice, LightEntity): Which results in a nice house disco :) """ - _brightness = 255 - - async def async_added_to_hass(self): - """Restore RFLink light brightness attribute.""" - await super().async_added_to_hass() - - old_state = await self.async_get_last_state() - if ( - old_state is not None - and old_state.attributes.get(ATTR_BRIGHTNESS) is not None - ): - # restore also brightness in dimmables devices - self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) - async def async_turn_on(self, **kwargs): """Turn the device on and set dim level.""" - if ATTR_BRIGHTNESS in kwargs: - # rflink only support 16 brightness levels - self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 - - # if receiver supports dimming this will turn on the light - # at the requested dim level - await self._async_handle_command("dim", self._brightness) - + await super().async_turn_on(**kwargs) # if the receiving device does not support dimlevel this # will ensure it is turned on when full brightness is set - if self._brightness == 255: + if self.brightness == 255: await self._async_handle_command("turn_on") - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - if self._brightness is None: - return {} - return {ATTR_BRIGHTNESS: self._brightness} - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - class ToggleRflinkLight(SwitchableRflinkDevice, LightEntity): """Rflink light device which sends out only 'on' commands. diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 1a616c2ed90..497c9b8cee6 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -2,7 +2,7 @@ from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, @@ -98,7 +98,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DATA_DEVICE_REGISTER][EVENT_KEY_SENSOR] = add_new_device -class RflinkSensor(RflinkDevice): +class RflinkSensor(RflinkDevice, SensorEntity): """Representation of a Rflink sensor.""" def __init__( diff --git a/homeassistant/components/rflink/utils.py b/homeassistant/components/rflink/utils.py new file mode 100644 index 00000000000..9738d9f74fa --- /dev/null +++ b/homeassistant/components/rflink/utils.py @@ -0,0 +1,11 @@ +"""RFLink integration utils.""" + + +def brightness_to_rflink(brightness: int) -> int: + """Convert 0-255 brightness to RFLink dim level (0-15).""" + return int(brightness / 17) + + +def rflink_to_brightness(dim_level: int) -> int: + """Convert RFLink dim level (0-15) to 0-255 brightness.""" + return int(dim_level * 17) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 5952cb62a71..d23a3e4e6ff 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -151,7 +151,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -DOMAINS = ["switch", "sensor", "light", "binary_sensor", "cover"] +PLATFORMS = ["switch", "sensor", "light", "binary_sensor", "cover"] async def async_setup(hass, config): @@ -202,9 +202,9 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): ) return False - for domain in DOMAINS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, domain) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -215,8 +215,8 @@ async def async_unload_entry(hass, entry: config_entries.ConfigEntry): if not all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in DOMAINS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ): @@ -428,7 +428,7 @@ def find_possible_pt2262_device(device_ids, device_id): if size is not None: size = len(dev_id) - size - 1 _LOGGER.info( - "rfxtrx: found possible device %s for %s " + "Found possible device %s for %s " "with the following configuration:\n" "data_bits=%d\n" "command_on=0x%s\n" @@ -505,7 +505,7 @@ class RfxtrxEntity(RestoreEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if not self._event: return None diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index c897e164119..72cd9f6bbf6 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + SensorEntity, ) from homeassistant.const import ( CONF_DEVICES, @@ -129,7 +130,7 @@ async def async_setup_entry( connect_auto_add(hass, discovery_info, sensor_update) -class RfxtrxSensor(RfxtrxEntity): +class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" def __init__(self, device, device_id, data_type, event=None): diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index b1e4197c0f1..0f3837f3a59 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -38,19 +38,33 @@ "options": { "error": { "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "invalid_event_code": "Ung\u00fcltiger Ereigniscode", + "invalid_input_2262_off": "Ung\u00fcltige Eingabe f\u00fcr Befehl \"aus\"", + "invalid_input_2262_on": "Ung\u00fcltige Eingabe f\u00fcr Befehl \"an\"", + "invalid_input_off_delay": "Ung\u00fcltige Eingabe f\u00fcr Ausschaltverz\u00f6gerung", "unknown": "Unerwarteter Fehler" }, "step": { "prompt_options": { "data": { - "debug": "Debugging aktivieren" - } + "automatic_add": "Automatisches Hinzuf\u00fcgen aktivieren", + "debug": "Debugging aktivieren", + "device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen", + "remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen" + }, + "title": "Rfxtrx Optionen" }, "set_device_options": { "data": { + "command_off": "Datenbitwert f\u00fcr den Befehl \"aus\"", + "command_on": "Datenbitwert f\u00fcr den Befehl \"ein\"", + "data_bit": "Anzahl der Datenbits", + "fire_event": "Ger\u00e4teereignis aktivieren", "off_delay": "Ausschaltverz\u00f6gerung", "off_delay_enabled": "Ausschaltverz\u00f6gerung aktivieren", - "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll" + "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll", + "signal_repetitions": "Anzahl der Signalwiederholungen", + "venetian_blind_mode": "Jalousie-Modus" } } } diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index c0df7233458..8794b3913f1 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -5,9 +5,13 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "one": "Vide", + "other": "Vide" }, "step": { + "one": "Vide", + "other": "Vide", "setup_network": { "data": { "host": "H\u00f4te", @@ -35,6 +39,7 @@ } } }, + "one": "Vide", "options": { "error": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", @@ -70,5 +75,6 @@ "title": "Configurer les options de l'appareil" } } - } + }, + "other": "Vide" } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 4e04ab16ce2..20ef3db6171 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -1,9 +1,19 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { + "setup_network": { + "data": { + "host": "Hoszt", + "port": "Port" + } + }, "setup_serial": { "data": { "device": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" @@ -11,13 +21,18 @@ "title": "Eszk\u00f6z" }, "setup_serial_manual_path": { + "data": { + "device": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, "title": "El\u00e9r\u00e9si \u00fat" } } }, "options": { "error": { - "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d" + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "prompt_options": { diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json new file mode 100644 index 00000000000..9836d252c68 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/id.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "setup_network": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Pilih alamat koneksi" + }, + "setup_serial": { + "data": { + "device": "Pilih perangkat" + }, + "title": "Perangkat" + }, + "setup_serial_manual_path": { + "data": { + "device": "Jalur Perangkat USB" + }, + "title": "Jalur" + }, + "user": { + "data": { + "type": "Jenis koneksi" + }, + "title": "Pilih jenis koneksi" + } + } + }, + "options": { + "error": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "invalid_event_code": "Kode event tidak valid", + "invalid_input_2262_off": "Masukan tidak valid untuk perintah mematikan", + "invalid_input_2262_on": "Masukan tidak valid untuk perintah menyalakan", + "invalid_input_off_delay": "Input tidak valid untuk penundaan mematikan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "Aktifkan penambahan otomatis", + "debug": "Aktifkan debugging", + "device": "Pilih perangkat untuk dikonfigurasi", + "event_code": "Masukkan kode event untuk ditambahkan", + "remove_device": "Pilih perangkat yang akan dihapus" + }, + "title": "Opsi Rfxtrx" + }, + "set_device_options": { + "data": { + "command_off": "Nilai bit data untuk perintah mematikan", + "command_on": "Nilai bit data untuk perintah menyalakan", + "data_bit": "Jumlah bit data", + "fire_event": "Aktifkan event perangkat", + "off_delay": "Penundaan mematikan", + "off_delay_enabled": "Aktifkan penundaan mematikan", + "replace_device": "Pilih perangkat yang akan diganti", + "signal_repetitions": "Jumlah pengulangan sinyal" + }, + "title": "Konfigurasi opsi perangkat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ko.json b/homeassistant/components/rfxtrx/translations/ko.json index e8c83a7bfd7..891926083dd 100644 --- a/homeassistant/components/rfxtrx/translations/ko.json +++ b/homeassistant/components/rfxtrx/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", + "already_configured": "\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.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -12,19 +12,63 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" - } + }, + "title": "\uc5f0\uacb0 \uc8fc\uc18c \uc120\ud0dd\ud558\uae30" + }, + "setup_serial": { + "data": { + "device": "\uae30\uae30 \uc120\ud0dd\ud558\uae30" + }, + "title": "\uae30\uae30" }, "setup_serial_manual_path": { "data": { "device": "USB \uc7a5\uce58 \uacbd\ub85c" - } + }, + "title": "\uacbd\ub85c" + }, + "user": { + "data": { + "type": "\uc5f0\uacb0 \uc720\ud615" + }, + "title": "\uc5f0\uacb0 \uc720\ud615 \uc120\ud0dd\ud558\uae30" } } }, "options": { "error": { "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_event_code": "\uc774\ubca4\ud2b8 \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_input_2262_off": "\ub044\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \uc785\ub825\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_input_2262_on": "\ucf1c\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \uc785\ub825\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_input_off_delay": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc\uc5d0 \ub300\ud55c \uc785\ub825\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "\uc790\ub3d9 \ucd94\uac00 \ud65c\uc131\ud654\ud558\uae30", + "debug": "\ub514\ubc84\uae45 \ud65c\uc131\ud654\ud558\uae30", + "device": "\uad6c\uc131\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30", + "event_code": "\ucd94\uac00\ud560 \uc774\ubca4\ud2b8 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "remove_device": "\uc0ad\uc81c\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30" + }, + "title": "Rfxtrx \uc635\uc158" + }, + "set_device_options": { + "data": { + "command_off": "\ub044\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \ub370\uc774\ud130 \ube44\ud2b8 \uac12", + "command_on": "\ucf1c\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \ub370\uc774\ud130 \ube44\ud2b8 \uac12", + "data_bit": "\ub370\uc774\ud130 \ube44\ud2b8 \uc218", + "fire_event": "\uae30\uae30 \uc774\ubca4\ud2b8 \ud65c\uc131\ud654\ud558\uae30", + "off_delay": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc", + "off_delay_enabled": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc(Off Delay) \ud65c\uc131\ud654\ud558\uae30", + "replace_device": "\uad50\uccb4\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30", + "signal_repetitions": "\uc2e0\ud638 \ubc18\ubcf5 \ud69f\uc218", + "venetian_blind_mode": "\ubca0\ub124\uc2dc\uc548 \ube14\ub77c\uc778\ub4dc \ubaa8\ub4dc" + }, + "title": "\uae30\uae30 \uc635\uc158 \uad6c\uc131\ud558\uae30" + } } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index 0b6e8997b18..1d22751ceed 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -39,16 +39,34 @@ "error": { "already_configured_device": "Apparaat is al geconfigureerd", "invalid_event_code": "Ongeldige gebeurteniscode", + "invalid_input_2262_off": "Ongeldige invoer voor commando uit", + "invalid_input_2262_on": "Ongeldige invoer voor commando aan", + "invalid_input_off_delay": "Ongeldige invoer voor uitschakelvertraging", "unknown": "Onverwachte fout" }, "step": { "prompt_options": { "data": { "automatic_add": "Schakel automatisch toevoegen in", - "debug": "Foutopsporing inschakelen" - } + "debug": "Foutopsporing inschakelen", + "device": "Selecteer het apparaat om te configureren", + "event_code": "Voer de gebeurteniscode in om toe te voegen", + "remove_device": "Apparaat selecteren dat u wilt verwijderen" + }, + "title": "Rfxtrx-opties" }, "set_device_options": { + "data": { + "command_off": "Waarde gegevensbits voor commando uit", + "command_on": "Waarde gegevensbits voor commando aan", + "data_bit": "Aantal databits", + "fire_event": "Schakel apparaatgebeurtenis in", + "off_delay": "Uitschakelvertraging", + "off_delay_enabled": "Schakel uitschakelvertraging in", + "replace_device": "Selecteer apparaat dat u wilt vervangen", + "signal_repetitions": "Aantal signaalherhalingen", + "venetian_blind_mode": "Venetiaanse jaloezie modus" + }, "title": "Configureer apparaatopties" } } diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index ed22575bccc..f5211ac54c0 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,10 +1,11 @@ """Support for Ring Doorbell/Chimes.""" +from __future__ import annotations + import asyncio from datetime import timedelta from functools import partial import logging from pathlib import Path -from typing import Optional from oauthlib.oauth2 import AccessDeniedError import requests @@ -99,9 +100,9 @@ async def async_setup_entry(hass, entry): ), } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) if hass.services.has_service(DOMAIN, "update"): @@ -126,8 +127,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -187,7 +188,7 @@ class GlobalDataUpdater: self._unsub_interval() self._unsub_interval = None - async def async_refresh_all(self, _now: Optional[int] = None) -> None: + async def async_refresh_all(self, _now: int | None = None) -> None: """Time to update.""" if not self.listeners: return diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index bbfbbf1690e..18ce87e722e 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -111,9 +111,9 @@ class RingBinarySensor(RingEntityMixin, BinarySensorEntity): return self._unique_id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - attrs = super().device_state_attributes + attrs = super().extra_state_attributes if self._active_alert is None: return attrs diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index bd5950b81a9..8f827aee7d2 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -93,7 +93,7 @@ class RingCam(RingEntityMixin, Camera): return self._device.id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 87b2026882c..a23a08b2a54 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries, const, core, exceptions -from . import DOMAIN # pylint: disable=unused-import +from . import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 6eb87cb8f9b..7a1c8ae7bdf 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -38,7 +38,7 @@ class RingEntityMixin: return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 0a1cc85230f..a20d484d3fe 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,7 +1,7 @@ """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.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from . import DOMAIN @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class RingSensor(RingEntityMixin, Entity): +class RingSensor(RingEntityMixin, SensorEntity): """A sensor implementation for Ring device.""" def __init__(self, config_entry_id, device, sensor_type): @@ -180,9 +180,9 @@ class HistoryRingSensor(RingSensor): return self._latest_event["created_at"].isoformat() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - attrs = super().device_state_attributes + attrs = super().extra_state_attributes if self._latest_event: attrs["created_at"] = self._latest_event["created_at"] diff --git a/homeassistant/components/ring/translations/he.json b/homeassistant/components/ring/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/ring/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/hu.json b/homeassistant/components/ring/translations/hu.json index fba6b944222..8e6e94eb2d6 100644 --- a/homeassistant/components/ring/translations/hu.json +++ b/homeassistant/components/ring/translations/hu.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.", - "unknown": "V\u00e1ratlan hiba" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "2fa": { diff --git a/homeassistant/components/ring/translations/id.json b/homeassistant/components/ring/translations/id.json new file mode 100644 index 00000000000..19883329938 --- /dev/null +++ b/homeassistant/components/ring/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kode autentikasi dua faktor" + }, + "title": "Autentikasi dua faktor" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Masuk dengan akun Ring" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/ru.json b/homeassistant/components/ring/translations/ru.json index 636d83f2e02..bc07db24007 100644 --- a/homeassistant/components/ring/translations/ru.json +++ b/homeassistant/components/ring/translations/ru.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Ring" } diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json index 5eb31bfa792..9f3c91e2a7c 100644 --- a/homeassistant/components/ring/translations/zh-Hant.json +++ b/homeassistant/components/ring/translations/zh-Hant.json @@ -10,9 +10,9 @@ "step": { "2fa": { "data": { - "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc" + "2fa": "\u96d9\u91cd\u8a8d\u8b49\u78bc" }, - "title": "\u96d9\u91cd\u9a57\u8b49" + "title": "\u96d9\u91cd\u8a8d\u8b49" }, "user": { "data": { diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index ab0da77b173..f36e2c58ec8 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -4,10 +4,9 @@ from datetime import timedelta from pyripple import get_balance import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTRIBUTION = "Data provided by ripple.com" @@ -31,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([RippleSensor(name, address)], True) -class RippleSensor(Entity): +class RippleSensor(SensorEntity): """Representation of an Ripple.com sensor.""" def __init__(self, name, address): @@ -57,7 +56,7 @@ class RippleSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 685fee43adf..eec30553870 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() events_coordinator = RiscoEventsDataUpdateCoordinator( hass, risco, entry.entry_id, 60 ) @@ -63,8 +63,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def start_platforms(): await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS ] ) await events_coordinator.async_refresh() @@ -79,8 +79,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index ba01b70686b..ba32429c154 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -60,7 +60,7 @@ class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): return binary_sensor_unique_id(self._risco, self._zone_id) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"zone_id": self._zone_id, "bypassed": self._zone.bypassed} diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 507e3943f2d..76b6105df01 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -23,9 +23,9 @@ from .const import ( CONF_HA_STATES_TO_RISCO, CONF_RISCO_STATES_TO_HA, DEFAULT_OPTIONS, + DOMAIN, RISCO_STATES, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 43d763a35fa..b39655949b2 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -1,5 +1,6 @@ """Sensor for Risco Events.""" from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -42,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class RiscoSensor(CoordinatorEntity): +class RiscoSensor(CoordinatorEntity, SensorEntity): """Sensor for Risco events.""" def __init__(self, coordinator, category_id, excludes, name, entry_id) -> None: @@ -94,7 +95,7 @@ class RiscoSensor(CoordinatorEntity): return self._event.time @property - def device_state_attributes(self): + def extra_state_attributes(self): """State attributes.""" if self._event is None: return None diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json index 3b2d79a34a7..ee57b9488dc 100644 --- a/homeassistant/components/risco/translations/hu.json +++ b/homeassistant/components/risco/translations/hu.json @@ -2,6 +2,27 @@ "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" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "pin": "PIN-k\u00f3d", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" + } } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/id.json b/homeassistant/components/risco/translations/id.json new file mode 100644 index 00000000000..eef1f00ffa6 --- /dev/null +++ b/homeassistant/components/risco/translations/id.json @@ -0,0 +1,55 @@ +{ + "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": { + "password": "Kata Sandi", + "pin": "Kode PIN", + "username": "Nama Pengguna" + } + } + } + }, + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "Diaktifkan untuk Keluar", + "armed_custom_bypass": "Diaktifkan Khusus", + "armed_home": "Diaktifkan untuk Di Rumah", + "armed_night": "Diaktifkan untuk Malam" + }, + "description": "Pilih status mana yang akan disetel pada Risco ketika mengaktifkan alarm Home Assistant", + "title": "Petakan status Home Assistant ke status Risco" + }, + "init": { + "data": { + "code_arm_required": "Membutuhkan Kode PIN untuk mengaktifkan", + "code_disarm_required": "Membutuhkan Kode PIN untuk menonaktifkan", + "scan_interval": "Seberapa sering untuk mengambil dari Risco (dalam detik)" + }, + "title": "Konfigurasi opsi" + }, + "risco_to_ha": { + "data": { + "A": "Grup A", + "B": "Grup B", + "C": "Grup C", + "D": "Grup D", + "arm": "Diaktifkan (Keluar)", + "partial_arm": "Diaktifkan Sebagian (Di Rumah)" + }, + "description": "Pilih status mana untuk masing-masing status yang akan dilaporkan oleh Home Assistant ke Risco", + "title": "Petakan status Risco ke status Home Assistant" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ko.json b/homeassistant/components/risco/translations/ko.json index f3065256e7f..6ceaedbc6dc 100644 --- a/homeassistant/components/risco/translations/ko.json +++ b/homeassistant/components/risco/translations/ko.json @@ -22,20 +22,21 @@ "step": { "ha_to_risco": { "data": { - "armed_away": "\uacbd\ube44\uc911(\uc678\ucd9c)", - "armed_custom_bypass": "\uacbd\ube44\uc911(\uc0ac\uc6a9\uc790 \uc6b0\ud68c)", - "armed_home": "\uc9d1\uc548 \uacbd\ube44\uc911", - "armed_night": "\uc57c\uac04 \uacbd\ube44\uc911" + "armed_away": "\uacbd\ube44 \uc911(\uc678\ucd9c)", + "armed_custom_bypass": "\uacbd\ube44 \uc911 (\uc0ac\uc6a9\uc790 \uc6b0\ud68c)", + "armed_home": "\uacbd\ube44 \uc911 (\uc7ac\uc2e4)", + "armed_night": "\uc57c\uac04 \uacbd\ube44 \uc911" }, - "description": "Home Assistant \uc54c\ub78c\uc744 \ud65c\uc131\ud654 \ud560 \ub54c Risco \uc54c\ub78c\uc758 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", - "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\ub85c \ub9e4\ud551" + "description": "Home Assistant \uc54c\ub78c\uc744 \uc124\uc815\ud560 \ub54c Risco \uc54c\ub78c\uc744 \uc124\uc815\ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uae30", + "title": "Home Assistant \uc0c1\ud0dc\ub97c Risco \uc0c1\ud0dc\uc5d0 \ub9e4\ud551\ud558\uae30" }, "init": { "data": { "code_arm_required": "\uc124\uc815\ud558\ub824\uba74 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4", "code_disarm_required": "\ud574\uc81c\ud558\ub824\uba74 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4", "scan_interval": "Risco\ub97c \ud3f4\ub9c1\ud558\ub294 \ube48\ub3c4 (\ucd08)" - } + }, + "title": "\uc635\uc158 \uad6c\uc131\ud558\uae30" }, "risco_to_ha": { "data": { @@ -43,11 +44,11 @@ "B": "\uadf8\ub8f9 B", "C": "\uadf8\ub8f9 C", "D": "\uadf8\ub8f9 D", - "arm": "\uacbd\ube44\uc911(\uc678\ucd9c)", - "partial_arm": "\ubd80\ubd84 \uacbd\ube44 \uc124\uc815 (\uc7ac\uc2e4)" + "arm": "\uacbd\ube44 \uc911 (\uc678\ucd9c)", + "partial_arm": "\ubd80\ubd84 \uacbd\ube44 \uc911 (\uc7ac\uc2e4)" }, - "description": "Risco\uc5d0\uc11c\ubcf4\uace0\ud558\ub294 \ubaa8\ub4e0 \uc0c1\ud0dc\uc5d0 \ub300\ud574 Home Assistant \uc54c\ub78c\uc774 \ubcf4\uace0 \ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4.", - "title": "Risco \uc0c1\ud0dc\ub97c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8 \uc0c1\ud0dc\uc5d0 \ub9e4\ud551" + "description": "Risco\uac00 \ubcf4\uace0\ud558\ub294 \ubaa8\ub4e0 \uc0c1\ud0dc\uc5d0 \ub300\ud574 Home Assistant \uc54c\ub78c\uc774 \ubcf4\uace0\ud560 \uc0c1\ud0dc\ub97c \uc120\ud0dd\ud558\uae30", + "title": "Risco \uc0c1\ud0dc\ub97c Home Assistant \uc0c1\ud0dc\uc5d0 \ub9e4\ud551\ud558\uae30" } } } diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index 97d0d454a4f..5267b164f3f 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -26,12 +26,15 @@ "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", "armed_home": "Ingeschakeld thuis", "armed_night": "Ingeschakeld nacht" - } + }, + "description": "Selecteer in welke staat u uw Risco-alarm wilt instellen wanneer u het Home Assistant-alarm inschakelt", + "title": "Wijs Home Assistant-staten toe aan Risco-staten" }, "init": { "data": { "code_arm_required": "PIN-code vereist om in te schakelen", - "code_disarm_required": "PIN-code vereist om uit te schakelen" + "code_disarm_required": "PIN-code vereist om uit te schakelen", + "scan_interval": "Polling-interval (in seconden)" }, "title": "Configureer opties" }, @@ -40,8 +43,12 @@ "A": "Groep A", "B": "Groep B", "C": "Groep C", - "D": "Groep D" - } + "D": "Groep D", + "arm": "Ingeschakeld (AFWEZIG)", + "partial_arm": "Gedeeltelijk ingeschakeld (AANWEZIG)" + }, + "description": "Selecteer welke staat uw Home Assistant alarm zal melden voor elke staat gemeld door Risco", + "title": "Wijs Risco-staten toe aan Home Assistant-staten" } } } diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json index a507bb84e53..200c38fb213 100644 --- a/homeassistant/components/risco/translations/ru.json +++ b/homeassistant/components/risco/translations/ru.json @@ -13,7 +13,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "pin": "PIN-\u043a\u043e\u0434", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index ba11206d496..f2fd13a9ef4 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -37,9 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -50,8 +50,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 59e442df538..7bd75cdbbc0 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ACCOUNT_HASH, DOMAIN # pylint:disable=unused-import +from .const import ACCOUNT_HASH, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 7041d22f4b8..471be52b054 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -66,7 +66,7 @@ class DiffuserSwitch(SwitchEntity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = { "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"], diff --git a/homeassistant/components/rituals_perfume_genie/translations/bg.json b/homeassistant/components/rituals_perfume_genie/translations/bg.json new file mode 100644 index 00000000000..cef3726d759 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/es.json b/homeassistant/components/rituals_perfume_genie/translations/es.json index bc74ecfd7ea..bdb1933eaf7 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/es.json +++ b/homeassistant/components/rituals_perfume_genie/translations/es.json @@ -1,7 +1,19 @@ { "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": { + "email": "email", + "password": "Contrase\u00f1a" + }, "title": "Con\u00e9ctese a su cuenta de Rituals" } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/hu.json b/homeassistant/components/rituals_perfume_genie/translations/hu.json new file mode 100644 index 00000000000..4ecaf2ba0d0 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/hu.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/id.json b/homeassistant/components/rituals_perfume_genie/translations/id.json new file mode 100644 index 00000000000..91d931005cf --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/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": { + "email": "Email", + "password": "Kata Sandi" + }, + "title": "Hubungkan ke akun Ritual Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/ko.json b/homeassistant/components/rituals_perfume_genie/translations/ko.json new file mode 100644 index 00000000000..489e4f5b806 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/ko.json @@ -0,0 +1,21 @@ +{ + "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": { + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "title": "Rituals \uacc4\uc815\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/pt.json b/homeassistant/components/rituals_perfume_genie/translations/pt.json new file mode 100644 index 00000000000..e3b78cd8e42 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index ad1ceea3d86..d85dae53303 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -10,11 +10,10 @@ from RMVtransport.rmvtransport import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TIMEOUT, TIME_MINUTES from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -104,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors) -class RMVDepartureSensor(Entity): +class RMVDepartureSensor(SensorEntity): """Implementation of an RMV departure sensor.""" def __init__( @@ -151,7 +150,7 @@ class RMVDepartureSensor(Entity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" try: return { diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index a75fc813fb9..4a349265459 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,8 +1,10 @@ """Support for Roku.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any from rokuecp import Roku, RokuConnectionError, RokuError from rokuecp.models import Device @@ -11,7 +13,6 @@ 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.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType @@ -38,7 +39,7 @@ SCAN_INTERVAL = timedelta(seconds=15) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: +async def async_setup(hass: HomeAssistantType, config: dict) -> bool: """Set up the Roku integration.""" hass.data.setdefault(DOMAIN, {}) return True @@ -47,16 +48,13 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -67,8 +65,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -151,7 +149,7 @@ class RokuEntity(CoordinatorEntity): return self._name @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this Roku device.""" if self._device_id is None: return None diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index b086d7a9311..8424850fe6c 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Roku.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from urllib.parse import urlparse from rokuecp import Roku, RokuError @@ -17,7 +19,7 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) @@ -27,7 +29,7 @@ ERROR_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: Dict) -> Dict: +async def validate_input(hass: HomeAssistantType, data: dict) -> dict: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -53,7 +55,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = {} @callback - def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + def _show_form(self, errors: dict | None = None) -> dict[str, Any]: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -61,9 +63,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_user( - self, user_input: Optional[Dict] = None - ) -> Dict[str, Any]: + async def async_step_user(self, user_input: dict | None = None) -> dict[str, Any]: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() @@ -115,8 +115,8 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() async def async_step_ssdp( - self, discovery_info: Optional[Dict] = None - ) -> Dict[str, Any]: + self, discovery_info: dict | None = None + ) -> dict[str, Any]: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] @@ -141,8 +141,8 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( - self, user_input: Optional[Dict] = None - ) -> Dict[str, Any]: + self, user_input: dict | None = None + ) -> dict[str, Any]: """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 10cef13cda0..981a9b08077 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.8.0"], + "requirements": ["rokuecp==0.8.1"], "homekit": { "models": [ "3810X", diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index e50c28d0a43..6fee53595ac 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,6 +1,7 @@ """Support for the Roku media player.""" +from __future__ import annotations + import logging -from typing import List, Optional import voluptuous as vol @@ -100,7 +101,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return self._unique_id @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device.""" if self.coordinator.data.info.device_type == "tv": return DEVICE_CLASS_TV @@ -230,7 +231,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None @property - def source_list(self) -> List: + def source_list(self) -> list: """List of available input sources.""" return ["Home"] + sorted(app.name for app in self.coordinator.data.apps) diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 3fcd2ee1a34..da578667578 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,5 +1,7 @@ """Support for the Roku remote.""" -from typing import Callable, List +from __future__ import annotations + +from typing import Callable from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry @@ -12,7 +14,7 @@ from .const import DOMAIN async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List, bool], None], + async_add_entities: Callable[[list, bool], None], ) -> bool: """Load Roku remote based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] @@ -56,7 +58,7 @@ class RokuRemote(RokuEntity, RemoteEntity): await self.coordinator.async_request_refresh() @roku_exception_handler - async def async_send_command(self, command: List, **kwargs) -> None: + async def async_send_command(self, command: list, **kwargs) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index 8b4e9f9d54d..5485d9e00ce 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -2,12 +2,26 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "unknown": "V\u00e1ratlan hiba" + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "data": { + "one": "Egy", + "other": "Egy\u00e9b" + }, + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {name}-t?", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", + "title": "Roku" + }, "user": { "data": { "host": "Hoszt" diff --git a/homeassistant/components/roku/translations/id.json b/homeassistant/components/roku/translations/id.json new file mode 100644 index 00000000000..0e60de9b61f --- /dev/null +++ b/homeassistant/components/roku/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Roku: {name}", + "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {name}?", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "Ingin menyiapkan {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Masukkan informasi Roku Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ko.json b/homeassistant/components/roku/translations/ko.json index 19f4c16785f..cb127234601 100644 --- a/homeassistant/components/roku/translations/ko.json +++ b/homeassistant/components/roku/translations/ko.json @@ -10,8 +10,12 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Roku" + }, "ssdp_confirm": { - "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Roku" }, "user": { diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index d892d2c78d2..daecee2f1dc 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Roku-apparaat is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "Roku: {name}", "step": { @@ -15,7 +15,7 @@ "one": "Een", "other": "Ander" }, - "description": "Wilt u {naam} instellen?", + "description": "Wilt u {name} instellen?", "title": "Roku" }, "ssdp_confirm": { @@ -28,7 +28,7 @@ }, "user": { "data": { - "host": "Host- of IP-adres" + "host": "Host" }, "description": "Voer uw Roku-informatie in." } diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 658c230c3a7..6de775e1d99 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -8,7 +8,7 @@ from roombapy import Roomba, RoombaConnectionError from homeassistant import exceptions from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD -from .const import BLID, COMPONENTS, CONF_BLID, CONF_CONTINUOUS, DOMAIN, ROOMBA_SESSION +from .const import BLID, CONF_BLID, CONF_CONTINUOUS, DOMAIN, PLATFORMS, ROOMBA_SESSION _LOGGER = logging.getLogger(__name__) @@ -51,9 +51,9 @@ async def async_setup_entry(hass, config_entry): BLID: config_entry.data[CONF_BLID], } - for component in COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) if not config_entry.update_listeners: @@ -105,8 +105,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in COMPONENTS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/roomba/braava.py b/homeassistant/components/roomba/braava.py index 1a3d106bf80..90298078e42 100644 --- a/homeassistant/components/roomba/braava.py +++ b/homeassistant/components/roomba/braava.py @@ -116,9 +116,9 @@ class BraavaJet(IRobotVacuum): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" - state_attrs = super().device_state_attributes + state_attrs = super().extra_state_attributes # Get Braava state state = self.vacuum_state diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 787382ed8b5..45c2d8b9a1b 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -18,20 +18,21 @@ from .const import ( CONF_CONTINUOUS, DEFAULT_CONTINUOUS, DEFAULT_DELAY, + DOMAIN, ROOMBA_SESSION, ) -from .const import DOMAIN # pylint:disable=unused-import ROOMBA_DISCOVERY_LOCK = "roomba_discovery_lock" +ALL_ATTEMPTS = 2 +HOST_ATTEMPTS = 6 +ROOMBA_WAKE_TIME = 6 DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELAY} MAX_NUM_DEVICES_TO_DISCOVER = 25 AUTH_HELP_URL_KEY = "auth_help_url" -AUTH_HELP_URL_VALUE = ( - "https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials" -) +AUTH_HELP_URL_VALUE = "https://www.home-assistant.io/integrations/roomba/#manually-retrieving-your-credentials" async def validate_input(hass: core.HomeAssistant, data): @@ -43,11 +44,13 @@ async def validate_input(hass: core.HomeAssistant, data): address=data[CONF_HOST], blid=data[CONF_BLID], password=data[CONF_PASSWORD], - continuous=data[CONF_CONTINUOUS], + continuous=False, delay=data[CONF_DELAY], ) info = await async_connect_or_timeout(hass, roomba) + if info: + await async_disconnect_or_timeout(hass, roomba) return { ROOMBA_SESSION: info[ROOMBA_SESSION], @@ -80,17 +83,26 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_host_already_configured(dhcp_discovery[IP_ADDRESS]): return self.async_abort(reason="already_configured") - if not dhcp_discovery[HOSTNAME].startswith("iRobot-"): + if not dhcp_discovery[HOSTNAME].startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") - blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME]) - await self.async_set_unique_id(blid) - self._abort_if_unique_id_configured( - updates={CONF_HOST: dhcp_discovery[IP_ADDRESS]} - ) - self.host = dhcp_discovery[IP_ADDRESS] - self.blid = blid + self.blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME]) + await self.async_set_unique_id(self.blid) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) + + # Because the hostname is so long some sources may + # truncate the hostname since it will be longer than + # the valid allowed length. If we already have a flow + # going for a longer hostname we abort so the user + # does not see two flows if discovery fails. + for progress in self._async_in_progress(): + flow_unique_id = progress["context"]["unique_id"] + if flow_unique_id.startswith(self.blid): + return self.async_abort(reason="short_blid") + if self.blid.startswith(flow_unique_id): + self.hass.config_entries.flow.async_abort(progress["flow_id"]) + self.context["title_placeholders"] = {"host": self.host, "name": self.blid} return await self.async_step_user() @@ -118,10 +130,8 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_start_link() already_configured = self._async_current_ids(False) - discovery = _async_get_roomba_discovery() - async with self.hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock()): - devices = await self.hass.async_add_executor_job(discovery.get_all) + devices = await _async_discover_roombas(self.hass, self.host) if devices: # Find already configured hosts @@ -130,13 +140,14 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for device in devices if device.blid not in already_configured } - if self.host and self.host in self.discovered_robots: - # From discovery - self.context["title_placeholders"] = { - "host": self.host, - "name": self.discovered_robots[self.host].robot_name, - } - return await self._async_start_link() + + if self.host and self.host in self.discovered_robots: + # From discovery + self.context["title_placeholders"] = { + "host": self.host, + "name": self.discovered_robots[self.host].robot_name, + } + return await self._async_start_link() if not self.discovered_robots: return await self.async_step_manual() @@ -180,7 +191,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") self.host = user_input[CONF_HOST] - self.blid = user_input[CONF_BLID] + self.blid = user_input[CONF_BLID].upper() await self.async_set_unique_id(self.blid, raise_on_progress=False) self._abort_if_unique_id_configured() return await self.async_step_link() @@ -197,11 +208,11 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={CONF_NAME: self.name or self.blid}, ) + roomba_pw = RoombaPassword(self.host) + try: - password = await self.hass.async_add_executor_job( - RoombaPassword(self.host).get_password - ) - except ConnectionRefusedError: + password = await self.hass.async_add_executor_job(roomba_pw.get_password) + except (OSError, ConnectionRefusedError): return await self.async_step_link_manual() if not password: @@ -220,7 +231,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except CannotConnect: return self.async_abort(reason="cannot_connect") - await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) self.name = info[CONF_NAME] return self.async_create_entry(title=self.name, data=config) @@ -242,7 +252,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {"base": "cannot_connect"} if not errors: - await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) return self.async_create_entry(title=info[CONF_NAME], data=config) return self.async_show_form( @@ -305,4 +314,41 @@ def _async_get_roomba_discovery(): @callback def _async_blid_from_hostname(hostname): """Extract the blid from the hostname.""" - return hostname.split("-")[1].split(".")[0] + return hostname.split("-")[1].split(".")[0].upper() + + +async def _async_discover_roombas(hass, host): + discovered_hosts = set() + devices = [] + discover_lock = hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock()) + discover_attempts = HOST_ATTEMPTS if host else ALL_ATTEMPTS + + for attempt in range(discover_attempts + 1): + async with discover_lock: + discovery = _async_get_roomba_discovery() + try: + if host: + discovered = [ + await hass.async_add_executor_job(discovery.get, host) + ] + else: + discovered = await hass.async_add_executor_job(discovery.get_all) + except OSError: + # Socket temporarily unavailable + await asyncio.sleep(ROOMBA_WAKE_TIME * attempt) + continue + else: + for device in discovered: + if device.ip in discovered_hosts: + continue + discovered_hosts.add(device.ip) + devices.append(device) + finally: + discovery.server_socket.close() + + if host and host in discovered_hosts: + return devices + + await asyncio.sleep(ROOMBA_WAKE_TIME) + + return devices diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 2ffb34eb7c8..0509cd92116 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -1,6 +1,6 @@ """The roomba constants.""" DOMAIN = "roomba" -COMPONENTS = ["sensor", "binary_sensor", "vacuum"] +PLATFORMS = ["sensor", "binary_sensor", "vacuum"] CONF_CERT = "certificate" CONF_CONTINUOUS = "continuous" CONF_BLID = "blid" diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 7dd045a1137..9d6a0f5cafc 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -168,7 +168,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" state = self.vacuum_state diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 5ceb44ff780..d1858a46fdc 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -5,5 +5,15 @@ "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": ["roombapy==1.6.2"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], - "dhcp": [{"hostname":"irobot-*","macaddress":"501479*"}] + "dhcp": [ + { + "hostname" : "irobot-*", + "macaddress" : "501479*" + }, + { + "hostname" : "roomba-*", + "macaddress" : "80A589*" + } + ] } + diff --git a/homeassistant/components/roomba/roomba.py b/homeassistant/components/roomba/roomba.py index 0a9aec0b608..5f960aeaae0 100644 --- a/homeassistant/components/roomba/roomba.py +++ b/homeassistant/components/roomba/roomba.py @@ -23,9 +23,9 @@ class RoombaVacuum(IRobotVacuum): """Basic Roomba robot (without carpet boost).""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" - state_attrs = super().device_state_attributes + state_attrs = super().extra_state_attributes # Get bin state bin_raw_state = self.vacuum_state.get("bin", {}) diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index f2d08f08772..4a99d9f71af 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -1,4 +1,5 @@ """Sensor for checking the battery level of Roomba.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.components.vacuum import STATE_DOCKED from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.icon import icon_for_battery_level @@ -16,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([roomba_vac], True) -class RoombaBattery(IRobotEntity): +class RoombaBattery(IRobotEntity, SensorEntity): """Class to hold Roomba Sensor basic info.""" @property diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index 48e130df4f5..16371041a15 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -3,7 +3,7 @@ "flow_title": "iRobot {name} ({host})", "step": { "init": { - "title": "Automaticlly connect to the device", + "title": "Automatically connect to the device", "description": "Select a Roomba or Braava.", "data": { "host": "[%key:common::config_flow::data::host%]" @@ -11,7 +11,7 @@ }, "manual": { "title": "Manually connect to the device", - "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-` or `Roomba-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "host": "[%key:common::config_flow::data::host%]", "blid": "BLID" @@ -19,7 +19,7 @@ }, "link": { "title": "Retrieve Password", - "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds)." + "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." }, "link_manual": { "title": "Enter Password", @@ -35,7 +35,8 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "not_irobot_device": "Discovered device is not an iRobot device" + "not_irobot_device": "Discovered device is not an iRobot device", + "short_blid": "The BLID was truncated" } }, "options": { diff --git a/homeassistant/components/roomba/translations/bg.json b/homeassistant/components/roomba/translations/bg.json new file mode 100644 index 00000000000..10f778472e6 --- /dev/null +++ b/homeassistant/components/roomba/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "link_manual": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index b2fe68c876c..3bdf842df9b 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", - "not_irobot_device": "El dispositiu descobert no \u00e9s un dispositiu iRobot" + "not_irobot_device": "El dispositiu descobert no \u00e9s un dispositiu iRobot", + "short_blid": "El BLID s'ha truncat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" @@ -15,10 +16,10 @@ "host": "Amfitri\u00f3" }, "description": "Selecciona un/a Roomba o Braava.", - "title": "Connecta't al dispositiu autom\u00e0ticament" + "title": "Connexi\u00f3 autom\u00e0tica amb el dispositiu" }, "link": { - "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons).", + "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons) despr\u00e9s, envia en els seg\u00fcents 30 segons.", "title": "Recupera la contrasenya" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "Amfitri\u00f3" }, - "description": "No s'ha descobert cap Roomba ni cap Braava a la teva xarxa. El BLID \u00e9s la part del nom d'amfitri\u00f3 del dispositiu despr\u00e9s de `iRobot-`. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}", + "description": "No s'ha descobert cap Roomba ni cap Braava a la teva xarxa. El BLID \u00e9s la part del nom d'amfitri\u00f3 del dispositiu despr\u00e9s de `iRobot-` o `Roomba-`. Segueix els passos de la documentaci\u00f3 seg\u00fcent: {auth_help_url}", "title": "Connecta't al dispositiu manualment" }, "user": { diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 9c373d649aa..5cc06f0cb5d 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", - "not_irobot_device": "Discovered device is not an iRobot device" + "not_irobot_device": "Discovered device is not an iRobot device", + "short_blid": "The BLID was truncated" }, "error": { "cannot_connect": "Failed to connect" @@ -15,10 +16,10 @@ "host": "Host" }, "description": "Select a Roomba or Braava.", - "title": "Automaticlly connect to the device" + "title": "Automatically connect to the device" }, "link": { - "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds).", + "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds.", "title": "Retrieve Password" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-` or `Roomba-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "title": "Manually connect to the device" }, "user": { diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index e038257c12d..7943df95b4f 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "cannot_connect": "\u00dchendamine nurjus", - "not_irobot_device": "Leitud seade ei ole iRoboti seade" + "not_irobot_device": "Leitud seade ei ole iRoboti seade", + "short_blid": "BLID'i k\u00e4rbiti" }, "error": { "cannot_connect": "\u00dchendamine nurjus" @@ -15,10 +16,10 @@ "host": "Host" }, "description": "Vali Roomba v\u00f5i Braava seade.", - "title": "\u00dchendu seadmega automaatselt" + "title": "\u00dchenda seadmega automaatselt" }, "link": { - "description": "Vajuta ja hoia all seadme {name} nuppu Home kuni seade teeb piiksu (umbes kaks sekundit).", + "description": "Vajuta ja hoia all seadme {name} nuppu Home kuni seade teeb piiksu (umbes kaks sekundit), edasta 30 sekundi jooksul.", "title": "Hangi salas\u00f5na" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "", "host": "Host" }, - "description": "V\u00f5rgus ei tuvastatud \u00fchtegi Roomba ega Braava seadet. BLID on seadme hostinime osa p\u00e4rast iRobot-`. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}", + "description": "V\u00f5rgus ei tuvastatud \u00fchtegi Roomba ega Braava seadet. BLID on seadme hostinime osa p\u00e4rast 'iRobot-` v\u00f5i 'Roomba-'. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}", "title": "\u00dchenda seadmega k\u00e4sitsi" }, "user": { diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/roomba/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 357ca74746d..8f7c2c97884 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -1,10 +1,51 @@ { "config": { + "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" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Hoszt" + }, + "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + }, + "link_manual": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Jelsz\u00f3 megad\u00e1sa" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Hoszt" + }, + "title": "Manu\u00e1lis csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + }, "user": { "data": { + "blid": "BLID", + "delay": "K\u00e9sleltet\u00e9s", "host": "Hoszt", "password": "Jelsz\u00f3" + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Folyamatos", + "delay": "K\u00e9sleltet\u00e9s" } } } diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json new file mode 100644 index 00000000000..3afe75ae09d --- /dev/null +++ b/homeassistant/components/roomba/translations/id.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "iRobot {name} ({host})", + "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Pilih Roomba atau Braava.", + "title": "Sambungkan secara otomatis ke perangkat" + }, + "link": { + "description": "Tekan dan tahan tombol Home pada {name} hingga perangkat mengeluarkan suara (sekitar dua detik).", + "title": "Ambil Kata Sandi" + }, + "link_manual": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi tidak dapat diambil dari perangkat secara otomatis. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}", + "title": "Masukkan Kata Sandi" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "Tidak ada Roomba atau Braava yang ditemukan di jaringan Anda. BLID adalah bagian dari nama host perangkat setelah `iRobot-`. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}", + "title": "Hubungkan ke perangkat secara manual" + }, + "user": { + "data": { + "blid": "BLID", + "continuous": "Terus menerus", + "delay": "Tunda", + "host": "Host", + "password": "Kata Sandi" + }, + "description": "Saat ini proses mengambil BLID dan kata sandi merupakan proses manual. Iikuti langkah-langkah yang diuraikan dalam dokumentasi di: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Hubungkan ke perangkat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Terus menerus", + "delay": "Tunda" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index b9e01faf16c..5e2e2b47141 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", - "not_irobot_device": "Il dispositivo rilevato non \u00e8 un dispositivo iRobot" + "not_irobot_device": "Il dispositivo rilevato non \u00e8 un dispositivo iRobot", + "short_blid": "Il BLID \u00e8 stato troncato" }, "error": { "cannot_connect": "Impossibile connettersi" @@ -18,7 +19,7 @@ "title": "Connettiti automaticamente al dispositivo" }, "link": { - "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi).", + "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi), quindi invialo entro 30 secondi.", "title": "Recupera password" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "Non sono stati trovati Roomba o Braava all'interno della tua rete. Il BLID \u00e8 la porzione del nome host del dispositivo dopo `iRobot-`. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}", + "description": "Nessun Roomba o Braava sono stati rilevati all'interno della tua rete. Il BLID \u00e8 la porzione del nome host del dispositivo dopo `iRobot-` o `Roomba-`. Segui i passaggi descritti nella documentazione all'indirizzo: {auth_help_url}", "title": "Connettiti manualmente al dispositivo" }, "user": { diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index d9c661a20dd..5066225100b 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -2,26 +2,39 @@ "config": { "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" + "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" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "\uc544\uc774\ub85c\ubd07: {name} ({host})", "step": { "init": { "data": { "host": "\ud638\uc2a4\ud2b8" - } + }, + "description": "\ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "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).", + "title": "\ube44\ubc00\ubc88\ud638 \uac00\uc838\uc624\uae30" }, "link_manual": { "data": { "password": "\ube44\ubc00\ubc88\ud638" - } + }, + "description": "\uae30\uae30\uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \uc790\ub3d9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 {auth_help_url} \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694.", + "title": "\ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" }, "manual": { "data": { + "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.", + "title": "\uae30\uae30\uc5d0 \uc218\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "user": { "data": { @@ -31,7 +44,7 @@ "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "\ud604\uc7ac BLID \ubc0f \ube44\ubc00\ubc88\ud638\ub294 \uc218\ub3d9\uc73c\ub85c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \ub2e4\uc74c \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \uc808\ucc28\ub97c \ub530\ub77c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "\ud604\uc7ac BLID \ubc0f \ube44\ubc00\ubc88\ud638\ub294 \uc218\ub3d9\uc73c\ub85c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \ub2e4\uc74c \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/roomba/translations/lb.json b/homeassistant/components/roomba/translations/lb.json index 500aa4fbee6..5578a7710db 100644 --- a/homeassistant/components/roomba/translations/lb.json +++ b/homeassistant/components/roomba/translations/lb.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Feeler beim verbannen" }, + "flow_title": "iRobot {name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index 0177adaea1f..f26b28d2248 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -6,9 +6,9 @@ "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "iRobot {naam} ({host})", + "flow_title": "iRobot {name} ({host})", "step": { "init": { "data": { @@ -18,7 +18,7 @@ "title": "Automatisch verbinding maken met het apparaat" }, "link": { - "description": "Houd de Home-knop op {naam} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden).", + "description": "Houd de Home-knop op {name} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden).", "title": "Wachtwoord opvragen" }, "link_manual": { @@ -32,14 +32,16 @@ "data": { "blid": "BLID", "host": "Host" - } + }, + "description": "Er is geen Roomba of Braava ontdekt op uw netwerk. De BLID is het gedeelte van de hostnaam van het apparaat na `iRobot-`. Volg de stappen die worden beschreven in de documentatie op: {auth_help_url}", + "title": "Handmatig verbinding maken met het apparaat" }, "user": { "data": { "blid": "BLID", "continuous": "Doorlopend", "delay": "Vertraging", - "host": "Hostnaam of IP-adres", + "host": "Host", "password": "Wachtwoord" }, "description": "Het ophalen van de BLID en het wachtwoord is momenteel een handmatig proces. Volg de stappen in de documentatie op: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index 979bb9bc70f..26e5c61faf3 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "not_irobot_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 iRobot." + "not_irobot_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 iRobot.", + "short_blid": "BLID \u0431\u044b\u043b \u0443\u043a\u0430\u0437\u0430\u043d \u043d\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." @@ -18,7 +19,7 @@ "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "link": { - "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434).", + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434). \u0417\u0430\u0442\u0435\u043c \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", "title": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u043e\u0432 Roomba \u0438\u043b\u0438 Braava. BLID - \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u043e\u0441\u043b\u0435 \"iRobot-\". \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439: {auth_help_url}.", + "description": "\u0412 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u043e\u0432 Roomba \u0438\u043b\u0438 Braava. BLID - \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u043e\u0441\u043b\u0435 `iRobot-` \u0438\u043b\u0438 `Roomba-`. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439: {auth_help_url}.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" }, "user": { diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index c6ed4436981..b1db9e39a25 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_HOST -from .const import ( # pylint: disable=unused-import +from .const import ( AUTHENTICATE_TIMEOUT, CONF_ROON_ID, DEFAULT_NAME, diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index b2ae62ec250..773028da2d3 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -465,7 +465,7 @@ class RoonDevice(MediaPlayerEntity): return for source in self.player_data["source_controls"]: - if source["supports_standby"] and not source["status"] == "indeterminate": + if source["supports_standby"] and source["status"] != "indeterminate": self._server.roonapi.standby(self.output_id, source["control_key"]) return diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 3b2d79a34a7..6ea0aa2b25c 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "duplicate_entry": "Ez a hoszt m\u00e1r konfigur\u00e1lva van.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/id.json b/homeassistant/components/roon/translations/id.json new file mode 100644 index 00000000000..bfd70955ac8 --- /dev/null +++ b/homeassistant/components/roon/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "duplicate_entry": "Host ini telah ditambahkan.", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "link": { + "description": "Anda harus mengotorisasi Home Assistant di Roon. Setelah Anda mengeklik kirim, buka aplikasi Roon Core, buka Pengaturan dan aktifkan HomeAssistant pada tab Ekstensi.", + "title": "Otorisasi HomeAssistant di Roon" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Tidak dapat menemukan server Roon, masukkan Nama Host atau IP Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json index 7c051d49dc7..b1e0fee4089 100644 --- a/homeassistant/components/roon/translations/ko.json +++ b/homeassistant/components/roon/translations/ko.json @@ -10,14 +10,14 @@ }, "step": { "link": { - "description": "Roon\uc5d0\uc11c \ud648 \uc5b4\uc2dc\uc2a4\ud134\ud2b8\ub97c \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4. \uc81c\ucd9c\uc744 \ud074\ub9ad \ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c HomeAssistant\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4.", - "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d" + "description": "Roon\uc5d0\uc11c Home Assistant\ub97c \uc778\uc99d\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud655\uc778\uc744 \ud074\ub9ad\ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c Home Assistant\ub97c \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694.", + "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d\ud558\uae30" }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Roon \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uba85\uc774\ub098 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + "description": "Roon \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 1061b7979ba..a2b6a854c62 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -1,7 +1,8 @@ """Update the IP addresses of your Route53 DNS records.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import List import boto3 import requests @@ -77,7 +78,7 @@ def _update_route53( aws_secret_access_key: str, zone: str, domain: str, - records: List[str], + records: list[str], ttl: int, ): _LOGGER.debug("Starting update for zone %s", zone) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index a2dafae9317..13f8fffb8d1 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -7,14 +7,13 @@ from requests.exceptions import ConnectTimeout, HTTPError from rova.rova import Rova import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, DEVICE_CLASS_TIMESTAMP, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle # Config for rova requests. @@ -80,7 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class RovaSensor(Entity): +class RovaSensor(SensorEntity): """Representation of a Rova sensor.""" def __init__(self, platform_name, sensor_key, data_service): diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 36d7ae50f32..318b29131b6 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -1,4 +1,7 @@ """Support for binary sensor using RPi GPIO.""" + +import asyncio + import voluptuous as vol from homeassistant.components import rpi_gpio @@ -52,6 +55,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class RPiGPIOBinarySensor(BinarySensorEntity): """Represent a binary sensor that uses Raspberry Pi GPIO.""" + async def async_read_gpio(self): + """Read state from GPIO.""" + await asyncio.sleep(float(self._bouncetime) / 1000) + self._state = await self.hass.async_add_executor_job( + rpi_gpio.read_input, self._port + ) + self.async_write_ha_state() + def __init__(self, name, port, pull_mode, bouncetime, invert_logic): """Initialize the RPi binary sensor.""" self._name = name or DEVICE_DEFAULT_NAME @@ -63,12 +74,11 @@ class RPiGPIOBinarySensor(BinarySensorEntity): rpi_gpio.setup_input(self._port, self._pull_mode) - def read_gpio(port): - """Read state from GPIO.""" - self._state = rpi_gpio.read_input(self._port) - self.schedule_update_ha_state() + def edge_detected(port): + """Edge detection handler.""" + self.hass.add_job(self.async_read_gpio) - rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) + rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime) @property def should_poll(self): diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 523d98dfdb7..1a73c736d04 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -2,6 +2,6 @@ "domain": "rpi_gpio", "name": "Raspberry Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", - "requirements": ["RPi.GPIO==0.7.0"], + "requirements": ["RPi.GPIO==0.7.1a4"], "codeowners": [] } diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index 9924ebf0440..b635972f43f 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -1,5 +1,7 @@ """Config flow for Raspberry Pi Power Supply Checker.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any from rpi_bad_power import new_under_voltage @@ -31,8 +33,8 @@ class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN): ) async def async_step_onboarding( - self, data: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, data: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initialized by onboarding.""" has_devices = await self._discovery_function(self.hass) diff --git a/homeassistant/components/rpi_power/translations/de.json b/homeassistant/components/rpi_power/translations/de.json index 9f3851f0c2b..1a00c87b985 100644 --- a/homeassistant/components/rpi_power/translations/de.json +++ b/homeassistant/components/rpi_power/translations/de.json @@ -1,11 +1,12 @@ { "config": { "abort": { + "no_devices_found": "Die f\u00fcr diese Komponente ben\u00f6tigte Systemklasse konnte nicht gefunden werden. Stellen Sie sicher, dass Ihr Kernel aktuell ist und die Hardware unterst\u00fctzt wird", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/rpi_power/translations/hu.json b/homeassistant/components/rpi_power/translations/hu.json new file mode 100644 index 00000000000..2d1c0811286 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "title": "Raspberry Pi Power Supply Checker" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/id.json b/homeassistant/components/rpi_power/translations/id.json new file mode 100644 index 00000000000..f9fcfa6c97a --- /dev/null +++ b/homeassistant/components/rpi_power/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak dapat menemukan kelas sistem yang diperlukan untuk komponen ini, pastikan kernel Anda cukup terbaru dan perangkat kerasnya didukung", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + } + } + }, + "title": "Pemeriksa Catu Daya Raspberry Pi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json index 445c0c34e68..e8423b7d733 100644 --- a/homeassistant/components/rpi_power/translations/ko.json +++ b/homeassistant/components/rpi_power/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + "no_devices_found": "\uc774 \uad6c\uc131 \uc694\uc18c\uc5d0 \ud544\uc694\ud55c \uc2dc\uc2a4\ud15c \ud074\ub798\uc2a4\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee4\ub110\uc774 \ucd5c\uc2e0 \uc0c1\ud0dc\uc774\uace0 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc9c0\uc6d0\ub418\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694", + "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." }, "step": { "confirm": { diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json index 8c15279dca8..5529aa39f20 100644 --- a/homeassistant/components/rpi_power/translations/nl.json +++ b/homeassistant/components/rpi_power/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "Kan de systeemklasse die nodig is voor dit onderdeel niet vinden, controleer of uw kernel recent is en of de hardware ondersteund wordt", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py index 4ac7283b194..a374300a264 100644 --- a/homeassistant/components/rpi_rf/switch.py +++ b/homeassistant/components/rpi_rf/switch.py @@ -45,7 +45,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -# pylint: disable=no-member def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" rpi_rf = importlib.import_module("rpi_rf") diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 3976d8985cd..4c02f49d86a 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -4,7 +4,7 @@ import xmlrpc.client import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, @@ -14,7 +14,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -75,7 +74,7 @@ def format_speed(speed): return round(kb_spd, 2 if kb_spd < 0.1 else 1) -class RTorrentSensor(Entity): +class RTorrentSensor(SensorEntity): """Representation of an rtorrent sensor.""" def __init__(self, sensor_type, rtorrent_client, client_name): diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index ed38f5a2a3a..2eb4f143131 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -47,9 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = RuckusUnleashedDataUpdateCoordinator(hass, ruckus=ruckus) - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() system_info = await hass.async_add_executor_job(ruckus.system_info) @@ -84,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 098ffba708c..26be0e5bed9 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -8,11 +8,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from .const import ( # pylint:disable=unused-import - API_SERIAL, - API_SYSTEM_OVERVIEW, - DOMAIN, -) +from .const import API_SERIAL, API_SYSTEM_OVERVIEW, DOMAIN _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 955e0581393..90a848b663b 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,5 +1,5 @@ """Support for Ruckus Unleashed devices.""" -from typing import Optional +from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -67,14 +67,17 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked): missing = [] for entity in registry.entities.values(): - if entity.config_entry_id == entry.entry_id and entity.platform == DOMAIN: - if entity.unique_id not in coordinator.data[API_CLIENTS]: - missing.append( - RuckusUnleashedDevice( - coordinator, entity.unique_id, entity.original_name - ) + if ( + entity.config_entry_id == entry.entry_id + and entity.platform == DOMAIN + and entity.unique_id not in coordinator.data[API_CLIENTS] + ): + missing.append( + RuckusUnleashedDevice( + coordinator, entity.unique_id, entity.original_name ) - tracked.add(entity.unique_id) + ) + tracked.add(entity.unique_id) if missing: async_add_entities(missing) @@ -115,7 +118,7 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): return SOURCE_TYPE_ROUTER @property - def device_info(self) -> Optional[dict]: + def device_info(self) -> dict | None: """Return the device information.""" if self.is_connected: return { diff --git a/homeassistant/components/ruckus_unleashed/translations/he.json b/homeassistant/components/ruckus_unleashed/translations/he.json new file mode 100644 index 00000000000..6ef580c7d8d --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/hu.json b/homeassistant/components/ruckus_unleashed/translations/hu.json index c1a23478ac4..0abcc301f0c 100644 --- a/homeassistant/components/ruckus_unleashed/translations/hu.json +++ b/homeassistant/components/ruckus_unleashed/translations/hu.json @@ -1,13 +1,17 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "unknown": "V\u00e1ratlan hiba" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { "data": { - "host": "Gazdag\u00e9p", + "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/ruckus_unleashed/translations/id.json b/homeassistant/components/ruckus_unleashed/translations/id.json new file mode 100644 index 00000000000..ed8fde32106 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/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", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/ru.json b/homeassistant/components/ruckus_unleashed/translations/ru.json index 9e0db9fcf94..9b02cafd466 100644 --- a/homeassistant/components/ruckus_unleashed/translations/ru.json +++ b/homeassistant/components/ruckus_unleashed/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 5e437c41b23..c0930f2c114 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -1,6 +1,6 @@ """Support for monitoring an SABnzbd NZB client.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import DATA_SABNZBD, SENSOR_TYPES, SIGNAL_SABNZBD_UPDATED @@ -18,7 +18,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class SabnzbdSensor(Entity): +class SabnzbdSensor(SensorEntity): """Representation of an SABnzbd sensor.""" def __init__(self, sensor_type, sabnzbd_api_data, client_name): diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 1e7b3dd5061..f1def71cc64 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -5,7 +5,7 @@ import logging import pysaj import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -26,7 +26,6 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -104,18 +103,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor in hass_sensors: state_unknown = False - if not values: - # SAJ inverters are powered by DC via solar panels and thus are - # offline after the sun has set. If a sensor resets on a daily - # basis like "today_yield", this reset won't happen automatically. - # Code below checks if today > day when sensor was last updated - # and if so: set state to None. - # Sensors with live values like "temperature" or "current_power" - # will also be reset to None. - if (sensor.per_day_basis and date.today() > sensor.date_updated) or ( - not sensor.per_day_basis and not sensor.per_total_basis - ): - state_unknown = True + # SAJ inverters are powered by DC via solar panels and thus are + # offline after the sun has set. If a sensor resets on a daily + # basis like "today_yield", this reset won't happen automatically. + # Code below checks if today > day when sensor was last updated + # and if so: set state to None. + # Sensors with live values like "temperature" or "current_power" + # will also be reset to None. + if not values and ( + (sensor.per_day_basis and date.today() > sensor.date_updated) + or (not sensor.per_day_basis and not sensor.per_total_basis) + ): + state_unknown = True sensor.async_update_values(unknown_state=state_unknown) return values @@ -160,7 +159,7 @@ def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: return remove_listener -class SAJsensor(Entity): +class SAJsensor(SensorEntity): """Representation of a SAJ sensor.""" def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 73d0d0b5e93..209b89f541a 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -21,7 +21,6 @@ from homeassistant.const import ( CONF_TOKEN, ) -# pylint:disable=unused-import from .bridge import SamsungTVBridge from .const import ( CONF_MANUFACTURER, diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 6e406f60ec4..a4b61369f99 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, ) +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -124,7 +125,7 @@ class SamsungTVDevice(MediaPlayerEntity): self.hass.add_job( self.hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=self._config_entry.data, ) ) diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index ca42aff331a..5c517c78d69 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.", - "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." diff --git a/homeassistant/components/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json new file mode 100644 index 00000000000..7d0f5982a65 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa setelan TV Anda untuk mengotorisasi Home Assistant.", + "cannot_connect": "Gagal terhubung", + "not_supported": "Perangkat TV Samsung ini saat ini tidak didukung." + }, + "flow_title": "TV Samsung: {model}", + "step": { + "confirm": { + "description": "Apakah Anda ingin menyiapkan TV Samsung {model}? Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi. Konfigurasi manual untuk TV ini akan ditimpa.", + "title": "TV Samsung" + }, + "user": { + "data": { + "host": "Host", + "name": "Nama" + }, + "description": "Masukkan informasi TV Samsung Anda. Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 14e35a7ff2e..7efb88bf7eb 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -3,14 +3,14 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", - "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", + "auth_missing": "Home Assistant\uac00 \ud574\ub2f9 \uc0bc\uc131 TV\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant\ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "flow_title": "\uc0bc\uc131 TV: {model}", "step": { "confirm": { - "description": "\uc0bc\uc131 TV {model} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV \uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc744 \ub36e\uc5b4\uc501\ub2c8\ub2e4.", + "description": "{model} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4.", "title": "\uc0bc\uc131 TV" }, "user": { @@ -18,7 +18,7 @@ "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" }, - "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4." + "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant\ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4." } } } diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index d1e2a9abaa2..2a6cca466ea 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Deze Samsung TV is al geconfigureerd.", - "already_in_progress": "Samsung TV configuratie is al in uitvoering.", + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.", "cannot_connect": "Kan geen verbinding maken", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund." @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Hostnaam of IP-adres", + "host": "Host", "name": "Naam" }, "description": "Voer uw Samsung TV informatie in. Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt." diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 245d21770ee..e11934c61c3 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,8 +1,10 @@ """Allow users to set and activate scenes.""" +from __future__ import annotations + import functools as ft import importlib import logging -from typing import Any, Optional +from typing import Any import voluptuous as vol @@ -94,7 +96,7 @@ class Scene(Entity): return False @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the scene.""" return STATE @@ -104,7 +106,6 @@ class Scene(Entity): async def async_activate(self, **kwargs: Any) -> None: """Activate scene. Try to get entities into requested state.""" - assert self.hass task = self.hass.async_add_job(ft.partial(self.activate, **kwargs)) if task: await task diff --git a/homeassistant/components/scene/translations/id.json b/homeassistant/components/scene/translations/id.json index 65f2adf7325..827c0c81f38 100644 --- a/homeassistant/components/scene/translations/id.json +++ b/homeassistant/components/scene/translations/id.json @@ -1,3 +1,3 @@ { - "title": "Adegan" + "title": "Scene" } \ No newline at end of file diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e8b6fcfd2c3..3bf070a7d79 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -6,7 +6,7 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol from homeassistant.components.rest.data import RestData -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_AUTHENTICATION, CONF_HEADERS, @@ -22,7 +22,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -89,7 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class ScrapeSensor(Entity): +class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" def __init__(self, rest, name, select, attr, index, value_template, unit): diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py new file mode 100644 index 00000000000..11f71cfd337 --- /dev/null +++ b/homeassistant/components/screenlogic/__init__.py @@ -0,0 +1,203 @@ +"""The Screenlogic integration.""" +import asyncio +from collections import defaultdict +from datetime import timedelta +import logging + +from screenlogicpy import ScreenLogicError, ScreenLogicGateway +from screenlogicpy.const import ( + EQUIPMENT, + SL_GATEWAY_IP, + SL_GATEWAY_NAME, + SL_GATEWAY_PORT, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .config_flow import async_discover_gateways_by_unique_id, name_for_mac +from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["switch", "sensor", "binary_sensor", "climate"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Screenlogic component.""" + domain_data = hass.data[DOMAIN] = {} + domain_data[DISCOVERED_GATEWAYS] = await async_discover_gateways_by_unique_id(hass) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Screenlogic from a config entry.""" + mac = entry.unique_id + # Attempt to re-discover named gateway to follow IP changes + discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS] + if mac in discovered_gateways: + connect_info = discovered_gateways[mac] + else: + _LOGGER.warning("Gateway rediscovery failed") + # Static connection defined or fallback from discovery + connect_info = { + SL_GATEWAY_NAME: name_for_mac(mac), + SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS], + SL_GATEWAY_PORT: entry.data[CONF_PORT], + } + + try: + gateway = ScreenLogicGateway(**connect_info) + except ScreenLogicError as ex: + _LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex) + raise ConfigEntryNotReady from ex + + # The api library uses a shared socket connection and does not handle concurrent + # requests very well. + api_lock = asyncio.Lock() + + coordinator = ScreenlogicDataUpdateCoordinator( + hass, config_entry=entry, gateway=gateway, api_lock=api_lock + ) + + device_data = defaultdict(list) + + 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) + ) + + 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 + ] + ) + ) + hass.data[DOMAIN][entry.entry_id]["listener"]() + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage the data update for the Screenlogic component.""" + + def __init__(self, hass, *, config_entry, gateway, api_lock): + """Initialize the Screenlogic Data Update Coordinator.""" + self.config_entry = config_entry + self.gateway = gateway + self.api_lock = api_lock + self.screenlogic_data = {} + interval = timedelta( + seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ) + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=interval, + ) + + async def _async_update_data(self): + """Fetch data from the Screenlogic gateway.""" + try: + async with self.api_lock: + await self.hass.async_add_executor_job(self.gateway.update) + except ScreenLogicError as error: + raise UpdateFailed(error) from error + return self.gateway.get_data() + + +class ScreenlogicEntity(CoordinatorEntity): + """Base class for all ScreenLogic entities.""" + + def __init__(self, coordinator, data_key): + """Initialize of the entity.""" + super().__init__(coordinator) + self._data_key = data_key + + @property + def mac(self): + """Mac address.""" + return self.coordinator.config_entry.unique_id + + @property + def unique_id(self): + """Entity Unique ID.""" + return f"{self.mac}_{self._data_key}" + + @property + def config_data(self): + """Shortcut for config data.""" + return self.coordinator.data["config"] + + @property + def gateway(self): + """Return the gateway.""" + return self.coordinator.gateway + + @property + def gateway_name(self): + """Return the configured name of the gateway.""" + return self.gateway.name + + @property + def device_info(self): + """Return device information for the controller.""" + controller_type = self.config_data["controller_type"] + hardware_type = self.config_data["hardware_type"] + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)}, + "name": self.gateway_name, + "manufacturer": "Pentair", + "model": EQUIPMENT.CONTROLLER_HARDWARE[controller_type][hardware_type], + } diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py new file mode 100644 index 00000000000..0001223030a --- /dev/null +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -0,0 +1,52 @@ +"""Support for a ScreenLogic Binary Sensor.""" +import logging + +from screenlogicpy.const import DEVICE_TYPE, ON_OFF + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) + +from . import ScreenlogicEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +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"] + + 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.""" + + @property + def name(self): + """Return the sensor name.""" + return f"{self.gateway_name} {self.sensor['name']}" + + @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) + + @property + def is_on(self) -> bool: + """Determine if the sensor is on.""" + return self.sensor["value"] == ON_OFF.ON + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data["sensors"][self._data_key] diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py new file mode 100644 index 00000000000..b50879bfd49 --- /dev/null +++ b/homeassistant/components/screenlogic/climate.py @@ -0,0 +1,220 @@ +"""Support for a ScreenLogic heating device.""" +import logging + +from screenlogicpy.const import EQUIPMENT, HEAT_MODE + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_PRESET_MODE, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.restore_state import RestoreEntity + +from . import ScreenlogicEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_FEATURES = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +SUPPORTED_MODES = [HVAC_MODE_OFF, HVAC_MODE_HEAT] + +SUPPORTED_PRESETS = [ + HEAT_MODE.SOLAR, + HEAT_MODE.SOLAR_PREFERRED, + HEAT_MODE.HEATER, +] + + +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"] + + for body in data["devices"]["body"]: + entities.append(ScreenLogicClimate(coordinator, body)) + async_add_entities(entities) + + +class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): + """Represents a ScreenLogic climate entity.""" + + def __init__(self, coordinator, body): + """Initialize a ScreenLogic climate entity.""" + super().__init__(coordinator, body) + self._configured_heat_modes = [] + # Is solar listed as available equipment? + if self.coordinator.data["config"]["equipment_flags"] & EQUIPMENT.FLAG_SOLAR: + self._configured_heat_modes.extend( + [HEAT_MODE.SOLAR, HEAT_MODE.SOLAR_PREFERRED] + ) + self._configured_heat_modes.append(HEAT_MODE.HEATER) + self._last_preset = None + + @property + def name(self) -> str: + """Name of the heater.""" + ent_name = self.body["heat_status"]["name"] + return f"{self.gateway_name} {ent_name}" + + @property + def min_temp(self) -> float: + """Minimum allowed temperature.""" + return self.body["min_set_point"]["value"] + + @property + def max_temp(self) -> float: + """Maximum allowed temperature.""" + return self.body["max_set_point"]["value"] + + @property + def current_temperature(self) -> float: + """Return water temperature.""" + return self.body["last_temperature"]["value"] + + @property + def target_temperature(self) -> float: + """Target temperature.""" + return self.body["heat_set_point"]["value"] + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + if self.config_data["is_celcius"]["value"] == 1: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + + @property + def hvac_mode(self) -> str: + """Return the current hvac mode.""" + if self.body["heat_mode"]["value"] > 0: + return HVAC_MODE_HEAT + return HVAC_MODE_OFF + + @property + def hvac_modes(self): + """Return th supported hvac modes.""" + return SUPPORTED_MODES + + @property + def hvac_action(self) -> str: + """Return the current action of the heater.""" + if self.body["heat_status"]["value"] > 0: + return CURRENT_HVAC_HEAT + if self.hvac_mode == HVAC_MODE_HEAT: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @property + def preset_mode(self) -> str: + """Return current/last preset mode.""" + if self.hvac_mode == HVAC_MODE_OFF: + return HEAT_MODE.NAME_FOR_NUM[self._last_preset] + return HEAT_MODE.NAME_FOR_NUM[self.body["heat_mode"]["value"]] + + @property + def preset_modes(self): + """All available presets.""" + return [ + HEAT_MODE.NAME_FOR_NUM[mode_num] for mode_num in self._configured_heat_modes + ] + + @property + def supported_features(self): + """Supported features of the heater.""" + return SUPPORTED_FEATURES + + async def async_set_temperature(self, **kwargs) -> None: + """Change the setpoint of the heater.""" + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") + + async with self.coordinator.api_lock: + success = await self.hass.async_add_executor_job( + self.gateway.set_heat_temp, int(self._data_key), int(temperature) + ) + + if success: + await self.coordinator.async_request_refresh() + else: + raise HomeAssistantError( + f"Failed to set_temperature {temperature} on body {self.body['body_type']['value']}" + ) + + async def async_set_hvac_mode(self, hvac_mode) -> None: + """Set the operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + mode = HEAT_MODE.OFF + else: + mode = HEAT_MODE.NUM_FOR_NAME[self.preset_mode] + + async with self.coordinator.api_lock: + success = await self.hass.async_add_executor_job( + self.gateway.set_heat_mode, int(self._data_key), int(mode) + ) + + if success: + await self.coordinator.async_request_refresh() + else: + raise HomeAssistantError( + f"Failed to set_hvac_mode {mode} on body {self.body['body_type']['value']}" + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + _LOGGER.debug("Setting last_preset to %s", HEAT_MODE.NUM_FOR_NAME[preset_mode]) + self._last_preset = mode = HEAT_MODE.NUM_FOR_NAME[preset_mode] + if self.hvac_mode == HVAC_MODE_OFF: + return + + async with self.coordinator.api_lock: + success = await self.hass.async_add_executor_job( + self.gateway.set_heat_mode, int(self._data_key), int(mode) + ) + + if success: + await self.coordinator.async_request_refresh() + else: + raise HomeAssistantError( + f"Failed to set_preset_mode {mode} on body {self.body['body_type']['value']}" + ) + + async def async_added_to_hass(self): + """Run when entity is about to be added.""" + await super().async_added_to_hass() + + _LOGGER.debug("Startup last preset is %s", self._last_preset) + if self._last_preset is not None: + return + prev_state = await self.async_get_last_state() + if ( + prev_state is not None + and prev_state.attributes.get(ATTR_PRESET_MODE) is not None + ): + _LOGGER.debug( + "Startup setting last_preset to %s from prev_state", + HEAT_MODE.NUM_FOR_NAME[prev_state.attributes.get(ATTR_PRESET_MODE)], + ) + self._last_preset = HEAT_MODE.NUM_FOR_NAME[ + prev_state.attributes.get(ATTR_PRESET_MODE) + ] + else: + _LOGGER.debug( + "Startup setting last_preset to default (%s)", + self._configured_heat_modes[0], + ) + self._last_preset = self._configured_heat_modes[0] + + @property + def body(self): + """Shortcut to access body data.""" + return self.coordinator.data["bodies"][self._data_key] diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py new file mode 100644 index 00000000000..4f388722117 --- /dev/null +++ b/homeassistant/components/screenlogic/config_flow.py @@ -0,0 +1,217 @@ +"""Config flow for ScreenLogic.""" +import logging + +from screenlogicpy import ScreenLogicError, discover +from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT +from screenlogicpy.requests import login +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + +GATEWAY_SELECT_KEY = "selected_gateway" +GATEWAY_MANUAL_ENTRY = "manual" + +PENTAIR_OUI = "00-C0-33" + + +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) + _LOGGER.debug("Discovered hosts: %s", hosts) + except ScreenLogicError as ex: + _LOGGER.debug(ex) + return discovered_gateways + + for host in hosts: + mac = _extract_mac_from_name(host[SL_GATEWAY_NAME]) + discovered_gateways[mac] = host + + _LOGGER.debug("Discovered gateways: %s", discovered_gateways) + return discovered_gateways + + +def _extract_mac_from_name(name): + return format_mac(f"{PENTAIR_OUI}-{name.split(':')[1].strip()}") + + +def short_mac(mac): + """Short version of the mac as seen in the app.""" + return "-".join(mac.split(":")[3:]).upper() + + +def name_for_mac(mac): + """Derive the gateway name from the mac.""" + return f"Pentair: {short_mac(mac)}" + + +async def async_get_mac_address(hass, ip_address, port): + """Connect to a screenlogic gateway and return the mac address.""" + connected_socket = await hass.async_add_executor_job( + login.create_socket, + ip_address, + port, + ) + if not connected_socket: + raise ScreenLogicError("Unknown socket error") + return await hass.async_add_executor_job(login.gateway_connect, connected_socket) + + +class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow to setup screen logic devices.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize ScreenLogic ConfigFlow.""" + self.discovered_gateways = {} + self.discovered_ip = None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for ScreenLogic.""" + return ScreenLogicOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass) + return await self.async_step_gateway_select() + + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + mac = _extract_mac_from_name(dhcp_discovery[HOSTNAME]) + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: dhcp_discovery[IP_ADDRESS]} + ) + self.discovered_ip = dhcp_discovery[IP_ADDRESS] + self.context["title_placeholders"] = {"name": dhcp_discovery[HOSTNAME]} + return await self.async_step_gateway_entry() + + async def async_step_gateway_select(self, user_input=None): + """Handle the selection of a discovered ScreenLogic gateway.""" + existing = self._async_current_ids() + unconfigured_gateways = { + mac: gateway[SL_GATEWAY_NAME] + for mac, gateway in self.discovered_gateways.items() + if mac not in existing + } + + if not unconfigured_gateways: + return await self.async_step_gateway_entry() + + errors = {} + if user_input is not None: + if user_input[GATEWAY_SELECT_KEY] == GATEWAY_MANUAL_ENTRY: + return await self.async_step_gateway_entry() + + mac = user_input[GATEWAY_SELECT_KEY] + selected_gateway = self.discovered_gateways[mac] + await self.async_set_unique_id(mac, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name_for_mac(mac), + data={ + CONF_IP_ADDRESS: selected_gateway[SL_GATEWAY_IP], + CONF_PORT: selected_gateway[SL_GATEWAY_PORT], + }, + ) + + return self.async_show_form( + step_id="gateway_select", + data_schema=vol.Schema( + { + vol.Required(GATEWAY_SELECT_KEY): vol.In( + { + **unconfigured_gateways, + GATEWAY_MANUAL_ENTRY: "Manually configure a ScreenLogic gateway", + } + ) + } + ), + errors=errors, + description_placeholders={}, + ) + + async def async_step_gateway_entry(self, user_input=None): + """Handle the manual entry of a ScreenLogic gateway.""" + errors = {} + ip_address = self.discovered_ip + port = 80 + + if user_input is not None: + ip_address = user_input[CONF_IP_ADDRESS] + port = user_input[CONF_PORT] + try: + mac = format_mac( + await async_get_mac_address(self.hass, ip_address, port) + ) + except ScreenLogicError as ex: + _LOGGER.debug(ex) + errors[CONF_IP_ADDRESS] = "cannot_connect" + + if not errors: + await self.async_set_unique_id(mac, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=name_for_mac(mac), + data={ + CONF_IP_ADDRESS: ip_address, + CONF_PORT: port, + }, + ) + + return self.async_show_form( + step_id="gateway_entry", + data_schema=vol.Schema( + { + vol.Required(CONF_IP_ADDRESS, default=ip_address): str, + vol.Required(CONF_PORT, default=port): int, + } + ), + errors=errors, + description_placeholders={}, + ) + + +class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow): + """Handles the options for the ScreenLogic integration.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Init the screen logic options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title=self.config_entry.title, data=user_input + ) + + current_interval = self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_SCAN_INTERVAL, + default=current_interval, + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)) + } + ), + description_placeholders={"gateway_name": self.config_entry.title}, + ) diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py new file mode 100644 index 00000000000..d777dc6ddc5 --- /dev/null +++ b/homeassistant/components/screenlogic/const.py @@ -0,0 +1,7 @@ +"""Constants for the ScreenLogic integration.""" + +DOMAIN = "screenlogic" +DEFAULT_SCAN_INTERVAL = 30 +MIN_SCAN_INTERVAL = 10 + +DISCOVERED_GATEWAYS = "_discovered_gateways" diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json new file mode 100644 index 00000000000..ab3d08a0702 --- /dev/null +++ b/homeassistant/components/screenlogic/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "screenlogic", + "name": "Pentair ScreenLogic", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/screenlogic", + "requirements": ["screenlogicpy==0.2.1"], + "codeowners": [ + "@dieselrabbit" + ], + "dhcp": [{"hostname":"pentair: *","macaddress":"00C033*"}] +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py new file mode 100644 index 00000000000..38bde2afd76 --- /dev/null +++ b/homeassistant/components/screenlogic/sensor.py @@ -0,0 +1,105 @@ +"""Support for a ScreenLogic Sensor.""" +import logging + +from screenlogicpy.const import DEVICE_TYPE + +from homeassistant.components.sensor import ( + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + SensorEntity, +) + +from . import ScreenlogicEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") + +SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { + DEVICE_TYPE.TEMPERATURE: DEVICE_CLASS_TEMPERATURE, + DEVICE_TYPE.ENERGY: DEVICE_CLASS_POWER, +} + + +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"] + # Generic sensors + for sensor in data["devices"]["sensor"]: + entities.append(ScreenLogicSensor(coordinator, sensor)) + # Pump sensors + for pump in data["devices"]["pump"]: + for pump_key in PUMP_SENSORS: + entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key)) + + async_add_entities(entities) + + +class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): + """Representation of a ScreenLogic sensor entity.""" + + @property + def name(self): + """Name of the sensor.""" + return f"{self.gateway_name} {self.sensor['name']}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.sensor.get("unit") + + @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) + + @property + def state(self): + """State of the sensor.""" + value = self.sensor["value"] + return (value - 1) if "supply" in self._data_key else value + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data["sensors"][self._data_key] + + +class ScreenLogicPumpSensor(ScreenlogicEntity, SensorEntity): + """Representation of a ScreenLogic pump sensor entity.""" + + def __init__(self, coordinator, pump, key): + """Initialize of the pump sensor.""" + super().__init__(coordinator, f"{key}_{pump}") + 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']}" + + @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) + + @property + def state(self): + """State of the pump sensor.""" + return self.pump_sensor["value"] + + @property + def pump_sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data["pumps"][self._pump_id][self._key] diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json new file mode 100644 index 00000000000..155eeb3043e --- /dev/null +++ b/homeassistant/components/screenlogic/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "flow_title": "ScreenLogic {name}", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "gateway_entry": { + "title": "ScreenLogic", + "description": "Enter your ScreenLogic Gateway information.", + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" + } + }, + "gateway_select": { + "title": "ScreenLogic", + "description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.", + "data": { + "selected_gateway": "Gateway" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options":{ + "step": { + "init": { + "title": "ScreenLogic", + "description": "Specify settings for {gateway_name}", + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py new file mode 100644 index 00000000000..e0077b1d62d --- /dev/null +++ b/homeassistant/components/screenlogic/switch.py @@ -0,0 +1,63 @@ +"""Support for a ScreenLogic 'circuit' switch.""" +import logging + +from screenlogicpy.const import ON_OFF + +from homeassistant.components.switch import SwitchEntity + +from . import ScreenlogicEntity +from .const import DOMAIN + +_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"] + + for switch in data["devices"]["switch"]: + entities.append(ScreenLogicSwitch(coordinator, switch)) + async_add_entities(entities) + + +class ScreenLogicSwitch(ScreenlogicEntity, SwitchEntity): + """ScreenLogic switch entity.""" + + @property + def name(self): + """Get the name of the switch.""" + return f"{self.gateway_name} {self.circuit['name']}" + + @property + def is_on(self) -> bool: + """Get whether the switch is in on state.""" + return self.circuit["value"] == 1 + + async def async_turn_on(self, **kwargs) -> None: + """Send the ON command.""" + return await self._async_set_circuit(ON_OFF.ON) + + async def async_turn_off(self, **kwargs) -> None: + """Send the OFF command.""" + return await self._async_set_circuit(ON_OFF.OFF) + + async def _async_set_circuit(self, circuit_value) -> None: + async with self.coordinator.api_lock: + success = await self.hass.async_add_executor_job( + self.gateway.set_circuit, self._data_key, circuit_value + ) + + if success: + _LOGGER.debug("Turn %s %s", self._data_key, circuit_value) + await self.coordinator.async_request_refresh() + else: + _LOGGER.warning( + "Failed to set_circuit %s %s", self._data_key, circuit_value + ) + + @property + def circuit(self): + """Shortcut to access the circuit.""" + return self.coordinator.data["circuits"][self._data_key] diff --git a/homeassistant/components/screenlogic/translations/ca.json b/homeassistant/components/screenlogic/translations/ca.json new file mode 100644 index 00000000000..68bfad1ff94 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/ca.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Adre\u00e7a IP", + "port": "Port" + }, + "description": "Introdueix la informaci\u00f3 de la passarel\u00b7la ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Passarel\u00b7la" + }, + "description": "S'han descobert les seg\u00fcents passarel\u00b7les ScreenLogic. Tria'n una per configurar-la o escull configurar manualment una passarel\u00b7la ScreenLogic.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segons entre escanejos" + }, + "description": "Especifica la configuraci\u00f3 de {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/cs.json b/homeassistant/components/screenlogic/translations/cs.json new file mode 100644 index 00000000000..a3fa8b759f8 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP adresa", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/de.json b/homeassistant/components/screenlogic/translations/de.json new file mode 100644 index 00000000000..6afe42e37ee --- /dev/null +++ b/homeassistant/components/screenlogic/translations/de.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP-Adresse", + "port": "Port" + }, + "description": "Gib deine ScreenLogic Gateway-Informationen ein.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "" + }, + "description": "Die folgenden ScreenLogic-Gateways wurden erkannt. Bitte w\u00e4hle eines aus, um es zu konfigurieren oder w\u00e4hle ein ScreenLogic-Gateway zum manuellen Konfigurieren.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunden zwischen den Scans" + }, + "description": "Einstellungen f\u00fcr {gateway_name} angeben", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/el.json b/homeassistant/components/screenlogic/translations/el.json new file mode 100644 index 00000000000..26906ac3b29 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/el.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03b1\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "\u03a0\u03cd\u03bb\u03b7" + }, + "description": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd \u03bf\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c0\u03cd\u03bb\u03b5\u03c2 ScreenLogic. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ae \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 ScreenLogic \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c3\u03b1\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd" + }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/en.json b/homeassistant/components/screenlogic/translations/en.json new file mode 100644 index 00000000000..2572fdf38fa --- /dev/null +++ b/homeassistant/components/screenlogic/translations/en.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP Address", + "port": "Port" + }, + "description": "Enter your ScreenLogic Gateway information.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + }, + "description": "Specify settings for {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/es-419.json b/homeassistant/components/screenlogic/translations/es-419.json new file mode 100644 index 00000000000..4e12c0c2a91 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/es-419.json @@ -0,0 +1,33 @@ +{ + "config": { + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Introduzca la informaci\u00f3n de su portal ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Portal" + }, + "description": "Se descubrieron los siguientes portales ScreenLogic. Seleccione uno para configurarlo, \u00f3 elija configurar manualmente el portal ScreenLogic.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre escaneos" + }, + "description": "Especificar la configuraci\u00f3n para {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json new file mode 100644 index 00000000000..8e9513d4f75 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Puerta de enlace" + }, + "description": "Se han descubierto las siguientes puertas de enlace ScreenLogic. Seleccione una para configurarla o elija configurar manualmente una puerta de enlace ScreenLogic.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre exploraciones" + }, + "description": "Especificar la configuraci\u00f3n de {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/et.json b/homeassistant/components/screenlogic/translations/et.json new file mode 100644 index 00000000000..cf2cf19418f --- /dev/null +++ b/homeassistant/components/screenlogic/translations/et.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP aadress", + "port": "Port" + }, + "description": "Sisesta oma ScreenLogic Gateway teave.", + "title": "" + }, + "gateway_select": { + "data": { + "selected_gateway": "L\u00fc\u00fcs" + }, + "description": "Avastati j\u00e4rgmised ScreenLogicu l\u00fc\u00fcsid. Vali seadistatav l\u00fc\u00fcs v\u00f5i seadista ScreenLogicu l\u00fc\u00fcs k\u00e4sitsi.", + "title": "" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "P\u00e4ringute vahe sekundites" + }, + "description": "M\u00e4\u00e4ra {gateway_name} s\u00e4tted", + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json new file mode 100644 index 00000000000..968045e0597 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "ScreenLogic {nom}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Adresse IP", + "port": "Port" + }, + "description": "Entrez vos informations de passerelle ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "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" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondes entre les scans" + }, + "description": "Sp\u00e9cifiez les param\u00e8tres pour {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/hu.json b/homeassistant/components/screenlogic/translations/hu.json new file mode 100644 index 00000000000..59e48fda273 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/hu.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP c\u00edm", + "port": "Port" + }, + "description": "Add meg a ScreenLogic Gateway adatait.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Szkennel\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" + }, + "description": "{gateway_name} be\u00e1ll\u00edt\u00e1sainak megad\u00e1sa", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/it.json b/homeassistant/components/screenlogic/translations/it.json new file mode 100644 index 00000000000..8fc3c346c0f --- /dev/null +++ b/homeassistant/components/screenlogic/translations/it.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Indirizzo IP", + "port": "Porta" + }, + "description": "Inserisci le informazioni del tuo gateway ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "Sono stati individuati i gateway ScreenLogic seguenti. Selezionarne uno da configurare oppure scegliere di configurare manualmente un gateway ScreenLogic.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondi tra le scansioni" + }, + "description": "Specifica le impostazioni per {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/ko.json b/homeassistant/components/screenlogic/translations/ko.json new file mode 100644 index 00000000000..94ddca6830a --- /dev/null +++ b/homeassistant/components/screenlogic/translations/ko.json @@ -0,0 +1,39 @@ +{ + "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" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + }, + "description": "ScreenLogic \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "\uac8c\uc774\ud2b8\uc6e8\uc774" + }, + "description": "\ub2e4\uc74c ScreenLogic \uac8c\uc774\ud2b8\uc6e8\uc774\uac00 \uac80\uc0c9\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uad6c\uc131\ud558\uac70\ub098 \uc218\ub3d9\uc73c\ub85c ScreenLogic \uac8c\uc774\ud2b8\uc6e8\uc774\ub85c \uad6c\uc131\ud560 \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" + }, + "description": "{gateway_name}\uc5d0 \ub300\ud55c \uc124\uc815 \uc9c0\uc815\ud558\uae30", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/nl.json b/homeassistant/components/screenlogic/translations/nl.json new file mode 100644 index 00000000000..7c752e0ae4d --- /dev/null +++ b/homeassistant/components/screenlogic/translations/nl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP-adres", + "port": "Poort" + }, + "description": "Voer uw ScreenLogic Gateway informatie in.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "De volgende ScreenLogic gateways werden ontdekt. Selecteer er een om te configureren, of kies ervoor om handmatig een ScreenLogic gateway te configureren.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconden tussen scans" + }, + "description": "Geef instellingen op voor {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/no.json b/homeassistant/components/screenlogic/translations/no.json new file mode 100644 index 00000000000..0ca4827514a --- /dev/null +++ b/homeassistant/components/screenlogic/translations/no.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP adresse", + "port": "Port" + }, + "description": "Skriv inn din ScreenLogic Gateway-informasjon.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "F\u00f8lgende ScreenLogic-gateways ble oppdaget. Velg en \u00e5 konfigurere, eller velg \u00e5 konfigurere en ScreenLogic gateway manuelt.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellom skanninger" + }, + "description": "Angi innstillinger for {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/pl.json b/homeassistant/components/screenlogic/translations/pl.json new file mode 100644 index 00000000000..64e2573ddb0 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/pl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Adres IP", + "port": "Port" + }, + "description": "Wprowad\u017a informacje o bramce ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Bramka" + }, + "description": "Wykryto nast\u0119puj\u0105ce bramki ScreenLogic. Wybierz jedn\u0105 do skonfigurowania lub wybierz opcj\u0119 r\u0119cznej konfiguracji bramki ScreenLogic.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "description": "Okre\u015bl ustawienia dla {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/ru.json b/homeassistant/components/screenlogic/translations/ru.json new file mode 100644 index 00000000000..a657b7360c7 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/ru.json @@ -0,0 +1,39 @@ +{ + "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." + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0448\u043b\u044e\u0437\u0435 ScreenLogic.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "\u0428\u043b\u044e\u0437" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0434\u0438\u043d \u0438\u0437 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0445 \u0448\u043b\u044e\u0437\u043e\u0432 ScreenLogic \u0438\u043b\u0438 \u0443\u043a\u0430\u0436\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/zh-Hant.json b/homeassistant/components/screenlogic/translations/zh-Hant.json new file mode 100644 index 00000000000..40ca94fd779 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/zh-Hant.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8f38\u5165 ScreenLogic \u9598\u9053\u5668\u8cc7\u8a0a\u3002", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "\u9598\u9053\u5668" + }, + "description": "\u641c\u5c0b\u5230\u4ee5\u4e0b ScreenLogic \u9598\u9053\u5668\uff0c\u8acb\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u9598\u9053\u5668\u3001\u6216\u9032\u884c\u624b\u52d5\u8a2d\u5b9a\u3002", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u9694\u79d2\u6578" + }, + "description": "{gateway_name} \u7279\u5b9a\u8a2d\u5b9a", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 5de3cb8264f..8f2e0743f77 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -1,16 +1,22 @@ """Support for scripts.""" +from __future__ import annotations + import asyncio import logging -from typing import List import voluptuous as vol 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, @@ -27,16 +33,19 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.script import ( ATTR_CUR, ATTR_MAX, - ATTR_MODE, 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 .trace import trace_script + _LOGGER = logging.getLogger(__name__) DOMAIN = "script" @@ -45,9 +54,10 @@ ATTR_LAST_ACTION = "last_action" ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" -CONF_DESCRIPTION = "description" +CONF_ADVANCED = "advanced" CONF_EXAMPLE = "example" CONF_FIELDS = "fields" +CONF_REQUIRED = "required" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -63,8 +73,13 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema( 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, } }, }, @@ -89,7 +104,7 @@ def is_on(hass, entity_id): @callback -def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: +def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all scripts that reference the entity.""" if DOMAIN not in hass.data: return [] @@ -104,7 +119,7 @@ def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: @callback -def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: +def entities_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all entities in script.""" if DOMAIN not in hass.data: return [] @@ -120,7 +135,7 @@ def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: @callback -def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]: +def scripts_with_device(hass: HomeAssistant, device_id: str) -> list[str]: """Return all scripts that reference the device.""" if DOMAIN not in hass.data: return [] @@ -135,7 +150,7 @@ def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]: @callback -def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: +def devices_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all devices in script.""" if DOMAIN not in hass.data: return [] @@ -150,6 +165,37 @@ def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: return list(script_entity.script.referenced_devices) +@callback +def scripts_with_area(hass: HomeAssistant, area_id: str) -> list[str]: + """Return all scripts that reference the area.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + script_entity.entity_id + for script_entity in component.entities + if area_id in script_entity.script.referenced_areas + ] + + +@callback +def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all areas in a script.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + script_entity = component.get_entity(entity_id) + + if script_entity is None: + return [] + + return list(script_entity.script.referenced_areas) + + async def async_setup(hass, config): """Load the scripts from the configuration.""" hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -220,7 +266,7 @@ async def _async_process_config(hass, config, component): ) script_entities = [ - ScriptEntity(hass, object_id, cfg) + ScriptEntity(hass, object_id, cfg, cfg.raw_config) for object_id, cfg in config.get(DOMAIN, {}).items() ] @@ -241,6 +287,7 @@ async def _async_process_config(hass, config, component): # Register the service description service_desc = { + CONF_NAME: script_entity.name, CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], CONF_FIELDS: cfg[CONF_FIELDS], } @@ -252,7 +299,7 @@ class ScriptEntity(ToggleEntity): icon = None - def __init__(self, hass, object_id, cfg): + def __init__(self, hass, object_id, cfg, raw_config): """Initialize the script.""" self.object_id = object_id self.icon = cfg.get(CONF_ICON) @@ -271,6 +318,7 @@ class ScriptEntity(ToggleEntity): variables=cfg.get(CONF_VARIABLES), ) self._changed = asyncio.Event() + self._raw_config = raw_config @property def should_poll(self): @@ -283,7 +331,7 @@ class ScriptEntity(ToggleEntity): return self.script.name @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = { ATTR_LAST_TRIGGERED: self.script.last_triggered, @@ -308,7 +356,11 @@ class ScriptEntity(ToggleEntity): self._changed.set() async def async_turn_on(self, **kwargs): - """Turn the script on.""" + """Run the script. + + Depending on the script's run mode, this may do nothing, restart the script or + fire an additional parallel run. + """ variables = kwargs.get("variables") context = kwargs.get("context") wait = kwargs.get("wait", True) @@ -318,7 +370,7 @@ class ScriptEntity(ToggleEntity): {ATTR_NAME: self.script.name, ATTR_ENTITY_ID: self.entity_id}, context=context, ) - coro = self.script.async_run(variables, context) + coro = self._async_run(variables, context) if wait: await coro return @@ -330,8 +382,20 @@ class ScriptEntity(ToggleEntity): self.hass.async_create_task(coro) await self._changed.wait() + async def _async_run(self, variables, context): + with trace_script( + self.hass, self.object_id, self._raw_config, context + ) as script_trace: + # Prepare tracing the execution of the script's sequence + script_trace.set_trace(trace_get()) + with trace_path("sequence"): + return await self.script.async_run(variables, context) + async def async_turn_off(self, **kwargs): - """Turn script off.""" + """Stop running the script. + + If multiple runs are in progress, all will be stopped. + """ await self.script.async_stop() async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index 3860a4d0119..5da8bec5a87 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -25,8 +25,21 @@ async def async_validate_config_item(hass, config, full_config=None): return config +class ScriptConfig(dict): + """Dummy class to allow adding attributes.""" + + raw_config = None + + async def _try_async_validate_config_item(hass, object_id, config, full_config=None): """Validate config item.""" + raw_config = None + try: + raw_config = dict(config) + except ValueError: + # Invalid config + pass + try: cv.slug(object_id) config = await async_validate_config_item(hass, config, full_config) @@ -34,6 +47,8 @@ async def _try_async_validate_config_item(hass, object_id, config, full_config=N async_log_exception(ex, DOMAIN, full_config or config, hass) return None + config = ScriptConfig(config) + config.raw_config = raw_config return config diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index b9d333ce553..ab14889a60c 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -2,6 +2,7 @@ "domain": "script", "name": "Scripts", "documentation": "https://www.home-assistant.io/integrations/script", + "dependencies": ["trace"], "codeowners": [ "@home-assistant/core" ], diff --git a/homeassistant/components/script/services.yaml b/homeassistant/components/script/services.yaml index 5af81734a9e..b772b80a1d2 100644 --- a/homeassistant/components/script/services.yaml +++ b/homeassistant/components/script/services.yaml @@ -1,16 +1,20 @@ # Describes the format for available python_script services reload: + name: Reload description: Reload all the available scripts turn_on: + name: Turn on description: Turn on script target: turn_off: + name: Turn off description: Turn off script target: toggle: + name: Toggle description: Toggle script target: diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py new file mode 100644 index 00000000000..a8053feaa1e --- /dev/null +++ b/homeassistant/components/script/trace.py @@ -0,0 +1,39 @@ +"""Trace support for script.""" +from __future__ import annotations + +from contextlib import contextmanager +from typing import Any + +from homeassistant.components.trace import ActionTrace, async_store_trace +from homeassistant.core import Context + + +class ScriptTrace(ActionTrace): + """Container for automation trace.""" + + def __init__( + self, + item_id: str, + config: dict[str, Any], + context: Context, + ): + """Container for automation trace.""" + key = ("script", item_id) + super().__init__(key, config, None, context) + + +@contextmanager +def trace_script(hass, item_id, config, context): + """Trace execution of a script.""" + trace = ScriptTrace(item_id, config, context) + async_store_trace(hass, trace) + + try: + yield trace + except Exception as ex: + if item_id: + trace.set_error(ex) + raise ex + finally: + if item_id: + trace.finished() diff --git a/homeassistant/components/script/translations/id.json b/homeassistant/components/script/translations/id.json index 8b23be94861..cac38736ea7 100644 --- a/homeassistant/components/script/translations/id.json +++ b/homeassistant/components/script/translations/id.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Skrip" diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 81e33aa24b5..3198f40720b 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -19,7 +19,6 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "search/related", @@ -38,12 +37,13 @@ async def async_setup(hass: HomeAssistant, config: dict): vol.Required("item_id"): str, } ) -async def websocket_search_related(hass, connection, msg): +@callback +def websocket_search_related(hass, connection, msg): """Handle search.""" searcher = Searcher( hass, - await device_registry.async_get_registry(hass), - await entity_registry.async_get_registry(hass), + device_registry.async_get(hass), + entity_registry.async_get(hass), ) connection.send_result( msg["id"], searcher.async_search(msg["item_type"], msg["item_id"]) @@ -127,6 +127,12 @@ class Searcher: ): self._add_or_resolve("entity", entity_entry.entity_id) + for entity_id in script.scripts_with_area(self.hass, area_id): + self._add_or_resolve("entity", entity_id) + + for entity_id in automation.automations_with_area(self.hass, area_id): + self._add_or_resolve("entity", entity_id) + @callback def _resolve_device(self, device_id) -> None: """Resolve a device.""" @@ -198,6 +204,9 @@ class Searcher: for device in automation.devices_in_automation(self.hass, automation_entity_id): self._add_or_resolve("device", device) + for area in automation.areas_in_automation(self.hass, automation_entity_id): + self._add_or_resolve("area", area) + @callback def _resolve_script(self, script_entity_id) -> None: """Resolve a script. @@ -210,6 +219,9 @@ class Searcher: for device in script.devices_in_script(self.hass, script_entity_id): self._add_or_resolve("device", device) + for area in script.areas_in_script(self.hass, script_entity_id): + self._add_or_resolve("area", area) + @callback def _resolve_group(self, group_entity_id) -> None: """Resolve a group. diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index e116fb0e861..165920dd8e5 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -6,10 +6,9 @@ import ephem import voluptuous as vol from homeassistant import util -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_TYPE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -109,7 +108,7 @@ def get_season(date, hemisphere, season_tracking_type): return HEMISPHERE_SEASON_SWAP.get(season) -class Season(Entity): +class Season(SensorEntity): """Representation of the current season.""" def __init__(self, hass, hemisphere, season_tracking_type, name): diff --git a/homeassistant/components/season/translations/sensor.id.json b/homeassistant/components/season/translations/sensor.id.json new file mode 100644 index 00000000000..fd28dca5901 --- /dev/null +++ b/homeassistant/components/season/translations/sensor.id.json @@ -0,0 +1,16 @@ +{ + "state": { + "season__season": { + "autumn": "Musim gugur", + "spring": "Musim semi", + "summer": "Musim panas", + "winter": "Musim dingin" + }, + "season__season__": { + "autumn": "Musim gugur", + "spring": "Musim semi", + "summer": "Musim panas", + "winter": "Musim dingin" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 1a93040837a..21ebcd828c2 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,6 +2,6 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.5.0"], + "requirements": ["sendgrid==6.6.0"], "codeowners": [] } diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 2d6c0c41e5b..1689d8c4834 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -139,9 +139,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) async def async_sense_update(_): @@ -169,8 +169,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index bc06721ae5e..ae5e4fc95bc 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -101,7 +101,7 @@ class SenseDevice(BinarySensorEntity): return self._id @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 866c1683b1e..4f88834eaca 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -8,8 +8,7 @@ from homeassistant import config_entries, core from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, SENSE_TIMEOUT_EXCEPTIONS -from .const import DOMAIN # pylint:disable=unused-import; pylint:disable=unused-import +from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 25fa5943bd5..0af64f3f17d 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring a Sense energy sensor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_POWER, @@ -8,7 +9,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import ( ACTIVE_NAME, @@ -118,7 +118,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices) -class SenseActiveSensor(Entity): +class SenseActiveSensor(SensorEntity): """Implementation of a Sense energy sensor.""" def __init__( @@ -163,7 +163,7 @@ class SenseActiveSensor(Entity): return POWER_WATT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -207,7 +207,7 @@ class SenseActiveSensor(Entity): self.async_write_ha_state() -class SenseVoltageSensor(Entity): +class SenseVoltageSensor(SensorEntity): """Implementation of a Sense energy voltage sensor.""" def __init__( @@ -247,7 +247,7 @@ class SenseVoltageSensor(Entity): return VOLT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -287,7 +287,7 @@ class SenseVoltageSensor(Entity): self.async_write_ha_state() -class SenseTrendsSensor(Entity): +class SenseTrendsSensor(SensorEntity): """Implementation of a Sense energy sensor.""" def __init__( @@ -333,7 +333,7 @@ class SenseTrendsSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -370,7 +370,7 @@ class SenseTrendsSensor(Entity): self.async_on_remove(self._coordinator.async_add_listener(self._async_update)) -class SenseEnergyDevice(Entity): +class SenseEnergyDevice(SensorEntity): """Implementation of a Sense energy device.""" def __init__(self, sense_devices_data, device, sense_monitor_id): @@ -415,7 +415,7 @@ class SenseEnergyDevice(Entity): return POWER_WATT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/sense/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json index 0085d9ea9c4..4ecaf2ba0d0 100644 --- a/homeassistant/components/sense/translations/hu.json +++ b/homeassistant/components/sense/translations/hu.json @@ -1,5 +1,13 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sense/translations/id.json b/homeassistant/components/sense/translations/id.json new file mode 100644 index 00000000000..8d0d996e510 --- /dev/null +++ b/homeassistant/components/sense/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": { + "email": "Email", + "password": "Kata Sandi" + }, + "title": "Hubungkan ke Sense Energy Monitor Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/ko.json b/homeassistant/components/sense/translations/ko.json index 517ad7af17d..269d8a76fea 100644 --- a/homeassistant/components/sense/translations/ko.json +++ b/homeassistant/components/sense/translations/ko.json @@ -14,7 +14,7 @@ "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" }, - "title": "Sense Energy Monitor \uc5d0 \uc5f0\uacb0\ud558\uae30" + "title": "Sense Energy Monitor\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json index ee9e61b5a38..df64e83da16 100644 --- a/homeassistant/components/sense/translations/nl.json +++ b/homeassistant/components/sense/translations/nl.json @@ -4,14 +4,14 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "email": "E-mailadres", + "email": "E-mail", "password": "Wachtwoord" }, "title": "Maak verbinding met uw Sense Energy Monitor" diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 67beb021d89..6ba00baae77 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -6,7 +6,7 @@ from pathlib import Path from sense_hat import SenseHat import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, @@ -14,7 +14,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -68,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) -class SenseHatSensor(Entity): +class SenseHatSensor(SensorEntity): """Representation of a Sense HAT sensor.""" def __init__(self, data, sensor_types): diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 26276650752..10ceaa39a38 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -191,7 +191,7 @@ class SensiboClimate(ClimateEntity): return self._external_state or super().state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"battery": self.current_battery} diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 46462019749..0012b1a3aa2 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -7,6 +7,8 @@ import voluptuous as vol from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -23,6 +25,7 @@ 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 # mypy: allow-untyped-defs, no-check-untyped-defs @@ -36,6 +39,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=30) DEVICE_CLASSES = [ DEVICE_CLASS_BATTERY, # % of battery that is left + DEVICE_CLASS_CO, # ppm (parts per million) Carbon Monoxide gas concentration + DEVICE_CLASS_CO2, # ppm (parts per million) Carbon Dioxide gas concentration DEVICE_CLASS_CURRENT, # current (A) DEVICE_CLASS_ENERGY, # energy (kWh, Wh) DEVICE_CLASS_HUMIDITY, # % of humidity in the air @@ -70,3 +75,7 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" return await hass.data[DOMAIN].async_unload_entry(entry) + + +class SensorEntity(Entity): + """Base class for sensor entities.""" diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index a9d44f2f860..fea79530485 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for sensors.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -14,6 +14,8 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_TYPE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -41,6 +43,8 @@ from . import DOMAIN DEVICE_CLASS_NONE = "none" CONF_IS_BATTERY_LEVEL = "is_battery_level" +CONF_IS_CO = "is_carbon_monoxide" +CONF_IS_CO2 = "is_carbon_dioxide" CONF_IS_CURRENT = "is_current" CONF_IS_ENERGY = "is_energy" CONF_IS_HUMIDITY = "is_humidity" @@ -56,6 +60,8 @@ CONF_IS_VALUE = "is_value" ENTITY_CONDITIONS = { DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], + DEVICE_CLASS_CO: [{CONF_TYPE: CONF_IS_CO}], + DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_IS_CO2}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], @@ -77,6 +83,8 @@ CONDITION_SCHEMA = vol.All( vol.Required(CONF_TYPE): vol.In( [ CONF_IS_BATTERY_LEVEL, + CONF_IS_CO, + CONF_IS_CO2, CONF_IS_CURRENT, CONF_IS_ENERGY, CONF_IS_HUMIDITY, @@ -101,9 +109,9 @@ CONDITION_SCHEMA = vol.All( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions.""" - conditions: List[Dict[str, str]] = [] + conditions: list[dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 86dda53cd2b..9586261a191 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -17,6 +17,8 @@ from homeassistant.const import ( CONF_FOR, CONF_TYPE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -39,6 +41,8 @@ from . import DOMAIN DEVICE_CLASS_NONE = "none" CONF_BATTERY_LEVEL = "battery_level" +CONF_CO = "carbon_monoxide" +CONF_CO2 = "carbon_dioxide" CONF_CURRENT = "current" CONF_ENERGY = "energy" CONF_HUMIDITY = "humidity" @@ -54,6 +58,8 @@ CONF_VALUE = "value" ENTITY_TRIGGERS = { DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + DEVICE_CLASS_CO: [{CONF_TYPE: CONF_CO}], + DEVICE_CLASS_CO2: [{CONF_TYPE: CONF_CO2}], DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}], DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], @@ -76,6 +82,8 @@ TRIGGER_SCHEMA = vol.All( vol.Required(CONF_TYPE): vol.In( [ CONF_BATTERY_LEVEL, + CONF_CO, + CONF_CO2, CONF_CURRENT, CONF_ENERGY, CONF_HUMIDITY, diff --git a/homeassistant/components/sensor/group.py b/homeassistant/components/sensor/group.py index 4741f8a3b54..2ac081496cd 100644 --- a/homeassistant/components/sensor/group.py +++ b/homeassistant/components/sensor/group.py @@ -2,13 +2,12 @@ from homeassistant.components.group import GroupIntegrationRegistry -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.exclude_domain() diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 2c281c0a046..cda80991242 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -1,5 +1,7 @@ """Helper to test significant sensor state changes.""" -from typing import Any, Optional, Union +from __future__ import annotations + +from typing import Any from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -19,7 +21,7 @@ def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs: Any, -) -> Optional[bool]: +) -> bool | None: """Test if state significantly changed.""" device_class = new_attrs.get(ATTR_DEVICE_CLASS) @@ -28,7 +30,7 @@ def async_check_significant_change( if device_class == DEVICE_CLASS_TEMPERATURE: if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - change: Union[float, int] = 1 + change: float | int = 1 else: change = 0.5 diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 76ea9efabc3..4298a367c2c 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -3,6 +3,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Current {entity_name} battery level", + "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", + "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", "is_power": "Current {entity_name} power", @@ -18,6 +20,8 @@ }, "trigger_type": { "battery_level": "{entity_name} battery level changes", + "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", + "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", "power": "{entity_name} power changes", diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index b351aed3049..e0cfb5faa50 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Nivell de bateria actual de {entity_name}", + "is_carbon_dioxide": "Concentraci\u00f3 actual de di\u00f2xid de carboni de {entity_name}", + "is_carbon_monoxide": "Concentraci\u00f3 actual de mon\u00f2xid de carboni de {entity_name}", "is_current": "Intensitat actual de {entity_name}", "is_energy": "Energia actual de {entity_name}", "is_humidity": "Humitat actual de {entity_name}", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "Canvia el nivell de bateria de {entity_name}", + "carbon_dioxide": "Canvia la concentraci\u00f3 de di\u00f2xid de carboni de {entity_name}", + "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", "current": "Canvia la intensitat de {entity_name}", "energy": "Canvia l'energia de {entity_name}", "humidity": "Canvia la humitat de {entity_name}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 0f72b344982..bb7c197f0e8 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "{entity_name} Batteriestand", + "is_carbon_dioxide": "Aktuelle {entity_name} Kohlenstoffdioxid-Konzentration", + "is_carbon_monoxide": "Aktuelle {entity_name} Kohlenstoffmonoxid-Konzentration", "is_humidity": "{entity_name} Feuchtigkeit", "is_illuminance": "Aktuelle {entity_name} Helligkeit", "is_power": "Aktuelle {entity_name} Leistung", @@ -13,6 +15,8 @@ }, "trigger_type": { "battery_level": "{entity_name} Batteriestatus\u00e4nderungen", + "carbon_dioxide": "{entity_name} Kohlenstoffdioxid-Konzentrations\u00e4nderung", + "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", "illuminance": "{entity_name} Helligkeits\u00e4nderungen", "power": "{entity_name} Leistungs\u00e4nderungen", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index ae0c32df574..e32ae845c1c 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Current {entity_name} battery level", + "is_carbon_dioxide": "Current {entity_name} carbon dioxide concentration level", + "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", "is_current": "Current {entity_name} current", "is_energy": "Current {entity_name} energy", "is_humidity": "Current {entity_name} humidity", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "{entity_name} battery level changes", + "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", + "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", "energy": "{entity_name} energy changes", "humidity": "{entity_name} humidity changes", diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index b2db3151abf..b810a3f0eb1 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", + "is_carbon_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de carbono {entity_name}", + "is_carbon_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de carbono {entity_name}", "is_current": "Corriente actual de {entity_name}", "is_energy": "Energ\u00eda actual de {entity_name}", "is_humidity": "Humedad actual de {entity_name}", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", + "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", "humidity": "Cambios de humedad de {entity_name}", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 450f5b60537..55b1fa48a8f 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Praegune {entity_name} aku tase", + "is_carbon_dioxide": "{entity_name} praegune s\u00fcsihappegaasi tase", + "is_carbon_monoxide": "{entity_name} praegune vingugaasi tase", "is_current": "Praegune {entity_name} voolutugevus", "is_energy": "Praegune {entity_name} v\u00f5imsus", "is_humidity": "Praegune {entity_name} niiskus", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "{entity_name} aku tase muutub", + "carbon_dioxide": "{entity_name} s\u00fcsihappegaasi tase muutus", + "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", "energy": "{entity_name} v\u00f5imsus muutub", "humidity": "{entity_name} niiskus muutub", diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index 4705d28b5c3..ef88b7acecc 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Niveau de la batterie de {entity_name}", + "is_carbon_dioxide": "Niveau actuel de concentration de dioxyde de carbone {entity_name}", + "is_carbon_monoxide": "Niveau actuel de concentration de monoxyde de carbone {entity_name}", "is_current": "Courant actuel pour {entity_name}", "is_energy": "\u00c9nergie actuelle pour {entity_name}", "is_humidity": "Humidit\u00e9 de {entity_name}", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "{entity_name} modification du niveau de batterie", + "carbon_dioxide": "{entity_name} changements de concentration de dioxyde de carbone", + "carbon_monoxide": "{entity_name} changements de concentration de monoxyde de carbone", "current": "{entity_name} changement de courant", "energy": "{entity_name} changement d'\u00e9nergie", "humidity": "{entity_name} modification de l'humidit\u00e9", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index 7be96984451..98e817fc164 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "{entity_name} aktu\u00e1lis akku szintje", + "is_carbon_dioxide": "Jelenlegi {entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3 szint", + "is_carbon_monoxide": "Jelenlegi {entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3 szint", "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", @@ -13,6 +15,8 @@ }, "trigger_type": { "battery_level": "{entity_name} akku szintje v\u00e1ltozik", + "carbon_dioxide": "{entity_name} sz\u00e9n-dioxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", + "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", @@ -20,7 +24,8 @@ "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", "timestamp": "{entity_name} id\u0151b\u00e9lyege v\u00e1ltozik", - "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik" + "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik", + "voltage": "{entity_name} fesz\u00fclts\u00e9ge v\u00e1ltozik" } }, "state": { diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index e2d0cdb057d..d43c2304428 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -1,8 +1,44 @@ { + "device_automation": { + "condition_type": { + "is_battery_level": "Level baterai {entity_name} saat ini", + "is_carbon_dioxide": "Level konsentasi karbondioksida {entity_name} saat ini", + "is_carbon_monoxide": "Level konsentasi karbonmonoksida {entity_name} saat ini", + "is_current": "Arus {entity_name} saat ini", + "is_energy": "Energi {entity_name} saat ini", + "is_humidity": "Kelembaban {entity_name} saat ini", + "is_illuminance": "Pencahayaan {entity_name} saat ini", + "is_power": "Daya {entity_name} saat ini", + "is_power_factor": "Faktor daya {entity_name} saat ini", + "is_pressure": "Tekanan {entity_name} saat ini", + "is_signal_strength": "Kekuatan sinyal {entity_name} saat ini", + "is_temperature": "Suhu {entity_name} saat ini", + "is_timestamp": "Stempel waktu {entity_name} saat ini", + "is_value": "Nilai {entity_name} saat ini", + "is_voltage": "Tegangan {entity_name} saat ini" + }, + "trigger_type": { + "battery_level": "Perubahan level baterai {entity_name}", + "carbon_dioxide": "Perubahan konsentrasi karbondioksida {entity_name}", + "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}", + "current": "Perubahan arus {entity_name}", + "energy": "Perubahan energi {entity_name}", + "humidity": "Perubahan kelembaban {entity_name}", + "illuminance": "Perubahan pencahayaan {entity_name}", + "power": "Perubahan daya {entity_name}", + "power_factor": "Perubahan faktor daya {entity_name}", + "pressure": "Perubahan tekanan {entity_name}", + "signal_strength": "Perubahan kekuatan sinyal {entity_name}", + "temperature": "Perubahan suhu {entity_name}", + "timestamp": "Perubahan stempel waktu {entity_name}", + "value": "Perubahan nilai {entity_name}", + "voltage": "Perubahan tegangan {entity_name}" + } + }, "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Sensor" diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 84a8b2773a5..c8b4a2f9b9c 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Livello della batteria attuale di {entity_name}", + "is_carbon_dioxide": "Livello di concentrazione di anidride carbonica attuale in {entity_name}", + "is_carbon_monoxide": "Livello attuale di concentrazione di monossido di carbonio in {entity_name}", "is_current": "Corrente attuale di {entity_name}", "is_energy": "Energia attuale di {entity_name}", "is_humidity": "Umidit\u00e0 attuale di {entity_name}", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "variazioni del livello di batteria di {entity_name} ", + "carbon_dioxide": "Variazioni della concentrazione di anidride carbonica di {entity_name}", + "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", "current": "variazioni di corrente di {entity_name}", "energy": "variazioni di energia di {entity_name}", "humidity": "variazioni di umidit\u00e0 di {entity_name} ", diff --git a/homeassistant/components/sensor/translations/ko.json b/homeassistant/components/sensor/translations/ko.json index 92fcd5d37a2..d8e99874822 100644 --- a/homeassistant/components/sensor/translations/ko.json +++ b/homeassistant/components/sensor/translations/ko.json @@ -1,26 +1,38 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 ~ \uc774\uba74", - "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4\uac00 ~ \uc774\uba74", - "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4\uac00 ~ \uc774\uba74", - "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825\uc774 ~ \uc774\uba74", - "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825\uc774 ~ \uc774\uba74", - "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74", - "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4\uac00 ~ \uc774\uba74", - "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01\uc774 ~ \uc774\uba74", - "is_value": "\ud604\uc7ac {entity_name} \uac12\uc774 ~ \uc774\uba74" + "is_battery_level": "\ud604\uc7ac {entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 ~ \uc774\uba74", + "is_carbon_dioxide": "\ud604\uc7ac {entity_name}\uc758 \uc774\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4 \uc218\uc900\uc774 ~ \uc774\uba74", + "is_carbon_monoxide": "\ud604\uc7ac {entity_name}\uc758 \uc77c\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4 \uc218\uc900\uc774 ~ \uc774\uba74", + "is_current": "\ud604\uc7ac {entity_name}\uc758 \uc804\ub958\uac00 ~ \uc774\uba74", + "is_energy": "\ud604\uc7ac {entity_name}\uc758 \uc5d0\ub108\uc9c0\uac00 ~ \uc774\uba74", + "is_humidity": "\ud604\uc7ac {entity_name}\uc758 \uc2b5\ub3c4\uac00 ~ \uc774\uba74", + "is_illuminance": "\ud604\uc7ac {entity_name}\uc758 \uc870\ub3c4\uac00 ~ \uc774\uba74", + "is_power": "\ud604\uc7ac {entity_name}\uc758 \uc18c\ube44 \uc804\ub825\uc774 ~ \uc774\uba74", + "is_power_factor": "\ud604\uc7ac {entity_name}\uc758 \uc5ed\ub960\uc774 ~ \uc774\uba74", + "is_pressure": "\ud604\uc7ac {entity_name}\uc758 \uc555\ub825\uc774 ~ \uc774\uba74", + "is_signal_strength": "\ud604\uc7ac {entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74", + "is_temperature": "\ud604\uc7ac {entity_name}\uc758 \uc628\ub3c4\uac00 ~ \uc774\uba74", + "is_timestamp": "\ud604\uc7ac {entity_name}\uc758 \uc2dc\uac01\uc774 ~ \uc774\uba74", + "is_value": "\ud604\uc7ac {entity_name}\uc758 \uac12\uc774 ~ \uc774\uba74", + "is_voltage": "\ud604\uc7ac {entity_name}\uc758 \uc804\uc555\uc774 ~ \uc774\uba74" }, "trigger_type": { - "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubc14\ub014 \ub54c", - "humidity": "{entity_name} \uc2b5\ub3c4\uac00 \ubc14\ub014 \ub54c", - "illuminance": "{entity_name} \uc870\ub3c4\uac00 \ubc14\ub014 \ub54c", - "power": "{entity_name} \uc18c\ube44 \uc804\ub825\uc774 \ubc14\ub014 \ub54c", - "pressure": "{entity_name} \uc555\ub825\uc774 \ubc14\ub014 \ub54c", - "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 \ubc14\ub014 \ub54c", - "temperature": "{entity_name} \uc628\ub3c4\uac00 \ubc14\ub014 \ub54c", - "timestamp": "{entity_name} \uc2dc\uac01\uc774 \ubc14\ub014 \ub54c", - "value": "{entity_name} \uac12\uc774 \ubc14\ub014 \ub54c" + "battery_level": "{entity_name}\uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubcc0\ud560 \ub54c", + "carbon_dioxide": "{entity_name}\uc758 \uc774\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4\uac00 \ubcc0\ud560 \ub54c", + "carbon_monoxide": "{entity_name}\uc758 \uc77c\uc0b0\ud654\ud0c4\uc18c \ub18d\ub3c4\uac00 \ubcc0\ud560 \ub54c", + "current": "{entity_name}\uc758 \uc804\ub958\uac00 \ubcc0\ud560 \ub54c", + "energy": "{entity_name}\uc758 \uc5d0\ub108\uc9c0\uac00 \ubcc0\ud560 \ub54c", + "humidity": "{entity_name}\uc758 \uc2b5\ub3c4\uac00 \ubcc0\ud560 \ub54c", + "illuminance": "{entity_name}\uc758 \uc870\ub3c4\uac00 \ubcc0\ud560 \ub54c", + "power": "{entity_name}\uc758 \uc18c\ube44 \uc804\ub825\uc774 \ubcc0\ud560 \ub54c", + "power_factor": "{entity_name}\uc758 \uc5ed\ub960\uc774 \ubcc0\ud560 \ub54c", + "pressure": "{entity_name}\uc758 \uc555\ub825\uc774 \ubcc0\ud560 \ub54c", + "signal_strength": "{entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 \ubcc0\ud560 \ub54c", + "temperature": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ubcc0\ud560 \ub54c", + "timestamp": "{entity_name}\uc758 \uc2dc\uac01\uc774 \ubcc0\ud560 \ub54c", + "value": "{entity_name}\uc758 \uac12\uc774 \ubcc0\ud560 \ub54c", + "voltage": "{entity_name}\uc758 \uc804\uc555\uc774 \ubcc0\ud560 \ub54c" } }, "state": { diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 869599296d5..411ebf3cefd 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Huidige batterijniveau {entity_name}", + "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie", + "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", "is_energy": "Huidige {entity_name} energie", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", @@ -17,16 +19,20 @@ }, "trigger_type": { "battery_level": "{entity_name} batterijniveau gewijzigd", + "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd", + "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", "energy": "{entity_name} energieveranderingen", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", "power": "{entity_name} vermogen gewijzigd", + "power_factor": "{entity_name} power factor verandert", "pressure": "{entity_name} druk gewijzigd", "signal_strength": "{entity_name} signaalsterkte gewijzigd", "temperature": "{entity_name} temperatuur gewijzigd", "timestamp": "{entity_name} tijdstip gewijzigd", - "value": "{entity_name} waarde gewijzigd" + "value": "{entity_name} waarde gewijzigd", + "voltage": "{entity_name} voltage verandert" } }, "state": { diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index b3e8dc199f6..3662356d15e 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5", + "is_carbon_dioxide": "Gjeldende {entity_name} karbondioksidkonsentrasjonsniv\u00e5", + "is_carbon_monoxide": "Gjeldende {entity_name} karbonmonoksid konsentrasjonsniv\u00e5", "is_current": "Gjeldende {entity_name} str\u00f8m", "is_energy": "Gjeldende {entity_name} effekt", "is_humidity": "Gjeldende {entity_name} fuktighet", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "{entity_name} batteriniv\u00e5 endres", + "carbon_dioxide": "{entity_name} endringer i konsentrasjonen av karbondioksid", + "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", "energy": "{entity_name} effektendringer", "humidity": "{entity_name} fuktighets endringer", diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index a8fb604cc53..a2baa380174 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "obecny poziom na\u0142adowania baterii {entity_name}", + "is_carbon_dioxide": "Bie\u017c\u0105cy poziom st\u0119\u017cenia dwutlenku w\u0119gla w {entity_name}", + "is_carbon_monoxide": "Bie\u017c\u0105cy poziom st\u0119\u017cenia tlenku w\u0119gla w {entity_name}", "is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}", "is_energy": "obecna energia {entity_name}", "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "zmieni si\u0119 poziom baterii {entity_name}", + "carbon_dioxide": "Zmiana st\u0119\u017cenie dwutlenku w\u0119gla w {entity_name}", + "carbon_monoxide": "Zmiana st\u0119\u017cenia tlenku w\u0119gla w {entity_name}", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index ae84c843bc3..ae0a0997dd6 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_carbon_dioxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u043b\u0435\u043a\u0438\u0441\u043b\u043e\u0433\u043e \u0433\u0430\u0437\u0430", + "is_carbon_monoxide": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0443\u0440\u043e\u0432\u043d\u044f \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u0438 \u0443\u0433\u0430\u0440\u043d\u043e\u0433\u043e \u0433\u0430\u0437\u0430", "is_current": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "is_energy": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "carbon_dioxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index 56350e501ef..ff84d8b2790 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -2,6 +2,8 @@ "device_automation": { "condition_type": { "is_battery_level": "\u76ee\u524d{entity_name}\u96fb\u91cf", + "is_carbon_dioxide": "\u76ee\u524d {entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", + "is_carbon_monoxide": "\u76ee\u524d {entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u72c0\u614b", "is_current": "\u76ee\u524d{entity_name}\u96fb\u6d41", "is_energy": "\u76ee\u524d{entity_name}\u96fb\u529b", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", @@ -17,6 +19,8 @@ }, "trigger_type": { "battery_level": "{entity_name}\u96fb\u91cf\u8b8a\u66f4", + "carbon_dioxide": "{entity_name} \u4e8c\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", + "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 6be02b9ba5e..c58d7bcd1a8 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -1,6 +1,7 @@ """The sentry integration.""" +from __future__ import annotations + import re -from typing import Dict, Union import sentry_sdk from sentry_sdk.integrations.aiohttp import AioHttpIntegration @@ -126,8 +127,8 @@ def process_before_send( options, channel: str, huuid: str, - system_info: Dict[str, Union[bool, str]], - custom_components: Dict[str, Integration], + system_info: dict[str, bool | str], + custom_components: dict[str, Integration], event, hint, ): diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index a308423f40b..b294fa46236 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, Optional +from typing import Any from sentry_sdk.utils import BadDsn, Dsn import voluptuous as vol @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback -from .const import ( # pylint: disable=unused-import +from .const import ( CONF_DSN, CONF_ENVIRONMENT, CONF_EVENT_CUSTOM_COMPONENTS, @@ -47,8 +47,8 @@ class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return SentryOptionsFlow(config_entry) async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a user config flow.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -78,8 +78,8 @@ class SentryOptionsFlow(config_entries.OptionsFlow): self.config_entry = config_entry async def async_step_init( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Manage Sentry options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index d0592493f15..04735d98687 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.20.3"], + "requirements": ["sentry-sdk==1.0.0"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 64ee672a02f..055c8817177 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -1,8 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "error": { "bad_dsn": "\u00c9rv\u00e9nytelen DSN", - "unknown": "V\u00e1ratlan hiba" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "user": { diff --git a/homeassistant/components/sentry/translations/id.json b/homeassistant/components/sentry/translations/id.json new file mode 100644 index 00000000000..fbd94fa6565 --- /dev/null +++ b/homeassistant/components/sentry/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "bad_dsn": "DSN tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + }, + "description": "Masukkan DSN Sentry Anda", + "title": "Sentry" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Nama opsional untuk lingkungan.", + "event_custom_components": "Kirim event dari komponen khusus", + "event_handled": "Kirim event yang ditangani", + "event_third_party_packages": "Kirim event dari paket pihak ketiga", + "tracing": "Aktifkan pelacakan kinerja", + "tracing_sample_rate": "Laju sampel pelacakan; antara 0,0 dan 1,0 (1,0 = 100%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/ko.json b/homeassistant/components/sentry/translations/ko.json index 6c00ffea2ef..92e26b30c42 100644 --- a/homeassistant/components/sentry/translations/ko.json +++ b/homeassistant/components/sentry/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "bad_dsn": "DSN \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -9,6 +9,9 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Sentry DSN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "Sentry" } @@ -18,14 +21,14 @@ "step": { "init": { "data": { - "environment": "\ud658\uacbd\uc758 \uc120\ud0dd\uc801 \uba85\uce6d", + "environment": "\ud658\uacbd\uc5d0 \ub300\ud55c \uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", "event_custom_components": "\uc0ac\uc6a9\uc790 \uc9c0\uc815 \uad6c\uc131 \uc694\uc18c\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", "event_handled": "\ucc98\ub9ac\ub41c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", - "event_third_party_packages": "\uc368\ub4dc\ud30c\ud2f0 \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", - "logging_event_level": "Log level Sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \uc774\ubca4\ud2b8\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4.", - "logging_level": "Log level sentry\ub294 \ub2e4\uc74c\uc5d0 \ub300\ud55c \ub85c\uadf8\ub97c \ube0c\ub808\ub4dc \ud06c\ub7fc\uc73c\ub85c \uae30\ub85d\ud569\ub2c8\ub2e4.", - "tracing": "\uc131\ub2a5 \ucd94\uc801 \ud65c\uc131\ud654", - "tracing_sample_rate": "\uc0d8\ud50c\ub9c1 \uc18d\ub3c4 \ucd94\uc801; 0.0\uc5d0\uc11c 1.0 \uc0ac\uc774 (1.0 = 100 %)" + "event_third_party_packages": "\uc11c\ub4dc \ud30c\ud2f0 \ud328\ud0a4\uc9c0\uc5d0\uc11c \uc774\ubca4\ud2b8 \ubcf4\ub0b4\uae30", + "logging_event_level": "\ub85c\uadf8 \ub808\ubca8 Sentry\ub294 \ub2e4\uc74c \ub85c\uadf8 \uc218\uc900\uc5d0 \ub300\ud55c \uc774\ubca4\ud2b8\ub97c \ub4f1\ub85d\ud569\ub2c8\ub2e4", + "logging_level": "\ub85c\uadf8 \ub808\ubca8 Sentry\ub294 \ub2e4\uc74c \ub85c\uadf8 \uc218\uc900\uc5d0 \ub300\ud55c \ub85c\uadf8\ub97c \ube0c\ub808\ub4dc\ud06c\ub7fc\uc73c\ub85c \uae30\ub85d\ud569\ub2c8\ub2e4", + "tracing": "\uc131\ub2a5 \ucd94\uc801 \ud65c\uc131\ud654\ud558\uae30", + "tracing_sample_rate": "\ucd94\uc801 \uc0d8\ud50c \uc18d\ub3c4; 0.0\uc5d0\uc11c 1.0 \uc0ac\uc774 (1.0 = 100%)" } } } diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json index 64b7f1b73f7..53f54ac1968 100644 --- a/homeassistant/components/sentry/translations/nl.json +++ b/homeassistant/components/sentry/translations/nl.json @@ -16,5 +16,21 @@ "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Optionele naam van de omgeving.", + "event_custom_components": "Gebeurtenissen verzenden vanuit aangepaste onderdelen", + "event_handled": "Stuur afgehandelde gebeurtenissen", + "event_third_party_packages": "Gebeurtenissen verzenden vanuit pakketten van derden", + "logging_event_level": "Het logniveau waarvoor Sentry een gebeurtenis registreert", + "logging_level": "Het logniveau Sentry zal logs opnemen als broodkruimels voor", + "tracing": "Schakel prestatietracering in", + "tracing_sample_rate": "Tracering van de steekproefsnelheid; tussen 0,0 en 1,0 (1,0 = 100%)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index b73a2e57f1a..aae10144a66 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -26,7 +26,7 @@ "event_handled": "\u50b3\u9001\u5df2\u8655\u7406\u4e8b\u4ef6", "event_third_party_packages": "\u50b3\u9001\u7b2c\u4e09\u65b9\u5c01\u5305\u4e8b\u4ef6", "logging_event_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u8a3b\u518a\u4e8b\u4ef6\u70ba", - "logging_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u7d00\u9304\u4e8b\u4ef6\u70ba\u6a94\u6848\u5c0e\u822a\u70ba", + "logging_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u65e5\u8a8c\u4e8b\u4ef6\u70ba\u6a94\u6848\u5c0e\u822a\u70ba", "tracing": "\u958b\u555f\u6548\u80fd\u8ffd\u8e64", "tracing_sample_rate": "\u8ffd\u8e64\u63a1\u6a23\u7bc4\u570d\uff0c\u4ecb\u65bc 0.0 \u53ca 1.0 \u4e4b\u9593\uff081.0 = 100%\uff09" } diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index e0bf23a2514..1e73ae9ac83 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -7,11 +7,10 @@ from serial import SerialException import serial_asyncio import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -103,7 +102,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([sensor], True) -class SerialSensor(Entity): +class SerialSensor(SensorEntity): """Representation of a Serial sensor.""" def __init__( @@ -241,7 +240,7 @@ class SerialSensor(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the entity (if any JSON present).""" return self._attributes diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 2e7604ee97d..b81c60e0a19 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -4,10 +4,9 @@ import logging from pmsensor import serial_pm as pm import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class ParticulateMatterSensor(Entity): +class ParticulateMatterSensor(SensorEntity): """Representation of an Particulate matter sensor.""" def __init__(self, pmDataCollector, name, pmname): diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index 0ad3d87a171..acd71b7c9e7 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_DEVICE_ID, CONF_API_KEY, STATE_LOCKED, STATE_UNLOCKED, @@ -14,7 +15,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -ATTR_DEVICE_ID = "device_id" ATTR_SERIAL_NO = "serial" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) @@ -86,7 +86,7 @@ class SesameDevice(LockEntity): self._responsive = status["responsive"] @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" return { ATTR_DEVICE_ID: self._device_id, diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 4f9f6514531..13f3cf22506 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,6 +2,6 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==8.1.1"], + "requirements": ["pillow==8.1.2"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 2cec9dea954..427882de91a 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -2,6 +2,6 @@ "domain": "seventeentrack", "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", - "requirements": ["py17track==2.2.2"], + "requirements": ["py17track==3.2.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index fa94ca4e384..e856f71b008 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -6,24 +6,24 @@ from py17track import Client as SeventeenTrackClient from py17track.errors import SeventeenTrackError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, ATTR_LOCATION, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, ) from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later from homeassistant.util import Throttle, slugify _LOGGER = logging.getLogger(__name__) ATTR_DESTINATION_COUNTRY = "destination_country" -ATTR_FRIENDLY_NAME = "friendly_name" ATTR_INFO_TEXT = "info_text" +ATTR_TIMESTAMP = "timestamp" ATTR_ORIGIN_COUNTRY = "origin_country" ATTR_PACKAGES = "packages" ATTR_PACKAGE_TYPE = "package_type" @@ -65,9 +65,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - websession = aiohttp_client.async_get_clientsession(hass) + session = aiohttp_client.async_get_clientsession(hass) - client = SeventeenTrackClient(websession) + client = SeventeenTrackClient(session=session) try: login_result = await client.profile.login( @@ -89,11 +89,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= scan_interval, config[CONF_SHOW_ARCHIVED], config[CONF_SHOW_DELIVERED], + str(hass.config.time_zone), ) await data.async_update() -class SeventeenTrackSummarySensor(Entity): +class SeventeenTrackSummarySensor(SensorEntity): """Define a summary sensor.""" def __init__(self, data, status, initial_state): @@ -109,7 +110,7 @@ class SeventeenTrackSummarySensor(Entity): return self._state is not None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs @@ -151,6 +152,7 @@ class SeventeenTrackSummarySensor(Entity): { ATTR_FRIENDLY_NAME: package.friendly_name, ATTR_INFO_TEXT: package.info_text, + ATTR_TIMESTAMP: package.timestamp, ATTR_STATUS: package.status, ATTR_LOCATION: package.location, ATTR_TRACKING_NUMBER: package.tracking_number, @@ -163,7 +165,7 @@ class SeventeenTrackSummarySensor(Entity): self._state = self._data.summary.get(self._status) -class SeventeenTrackPackageSensor(Entity): +class SeventeenTrackPackageSensor(SensorEntity): """Define an individual package sensor.""" def __init__(self, data, package): @@ -172,6 +174,7 @@ class SeventeenTrackPackageSensor(Entity): ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_DESTINATION_COUNTRY: package.destination_country, ATTR_INFO_TEXT: package.info_text, + ATTR_TIMESTAMP: package.timestamp, ATTR_LOCATION: package.location, ATTR_ORIGIN_COUNTRY: package.origin_country, ATTR_PACKAGE_TYPE: package.package_type, @@ -190,7 +193,7 @@ class SeventeenTrackPackageSensor(Entity): return self._data.packages.get(self._tracking_number) is not None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs @@ -237,7 +240,11 @@ class SeventeenTrackPackageSensor(Entity): return self._attrs.update( - {ATTR_INFO_TEXT: package.info_text, ATTR_LOCATION: package.location} + { + ATTR_INFO_TEXT: package.info_text, + ATTR_TIMESTAMP: package.timestamp, + ATTR_LOCATION: package.location, + } ) self._state = package.status self._friendly_name = package.friendly_name @@ -277,7 +284,13 @@ class SeventeenTrackData: """Define a data handler for 17track.net.""" def __init__( - self, client, async_add_entities, scan_interval, show_archived, show_delivered + self, + client, + async_add_entities, + scan_interval, + show_archived, + show_delivered, + timezone, ): """Initialize.""" self._async_add_entities = async_add_entities @@ -287,6 +300,7 @@ class SeventeenTrackData: self.account_id = client.profile.account_id self.packages = {} self.show_delivered = show_delivered + self.timezone = timezone self.summary = {} self.async_update = Throttle(self._scan_interval)(self._async_update) @@ -297,7 +311,7 @@ class SeventeenTrackData: try: packages = await self._client.profile.packages( - show_archived=self._show_archived + show_archived=self._show_archived, tz=self.timezone ) _LOGGER.debug("New package data received: %s", packages) diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index b7684f98855..94b6a8f2e3b 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -1,6 +1,7 @@ """Shark IQ Integration.""" import asyncio +from contextlib import suppress import async_timeout from sharkiqpy import ( @@ -14,7 +15,7 @@ from sharkiqpy import ( from homeassistant import exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import _LOGGER, API_TIMEOUT, COMPONENTS, DOMAIN +from .const import _LOGGER, API_TIMEOUT, DOMAIN, PLATFORMS from .update_coordinator import SharkIqUpdateCoordinator @@ -59,20 +60,17 @@ async def async_setup_entry(hass, config_entry): raise exceptions.ConfigEntryNotReady from exc shark_vacs = await ayla_api.async_get_devices(False) - device_names = ", ".join([d.name for d in shark_vacs]) + device_names = ", ".join(d.name for d in shark_vacs) _LOGGER.debug("Found %d Shark IQ device(s): %s", len(shark_vacs), device_names) coordinator = SharkIqUpdateCoordinator(hass, config_entry, ayla_api, shark_vacs) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise exceptions.ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][config_entry.entry_id] = coordinator - for component in COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -81,11 +79,10 @@ async def async_setup_entry(hass, config_entry): async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): """Disconnect to vacuum.""" _LOGGER.debug("Disconnecting from Ayla Api") - with async_timeout.timeout(5): - try: - await coordinator.ayla_api.async_sign_out() - except (SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError): - pass + with async_timeout.timeout(5), suppress( + SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError + ): + await coordinator.ayla_api.async_sign_out() async def async_update_options(hass, config_entry): @@ -98,17 +95,15 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in COMPONENTS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) if unload_ok: domain_data = hass.data[DOMAIN][config_entry.entry_id] - try: + with suppress(SharkIqAuthError): await async_disconnect_or_timeout(coordinator=domain_data) - except SharkIqAuthError: - pass hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 9d2e80b8ec6..046aaee7df5 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Shark IQ integration.""" +from __future__ import annotations import asyncio -from typing import Dict, Optional import aiohttp import async_timeout @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import _LOGGER, DOMAIN # pylint:disable=unused-import +from .const import _LOGGER, DOMAIN SHARKIQ_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} @@ -62,7 +62,7 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return info, errors - async def async_step_user(self, user_input: Optional[Dict] = None): + async def async_step_user(self, user_input: dict | None = None): """Handle the initial step.""" errors = {} if user_input is not None: @@ -76,7 +76,7 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=SHARKIQ_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input: Optional[dict] = None): + async def async_step_reauth(self, user_input: dict | None = None): """Handle re-auth if login is invalid.""" errors = {} diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py index e0feb306f77..8f4c56b65db 100644 --- a/homeassistant/components/sharkiq/const.py +++ b/homeassistant/components/sharkiq/const.py @@ -5,7 +5,7 @@ import logging _LOGGER = logging.getLogger(__package__) API_TIMEOUT = 20 -COMPONENTS = ["vacuum"] +PLATFORMS = ["vacuum"] DOMAIN = "sharkiq" SHARK = "Shark" UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/sharkiq/translations/hu.json b/homeassistant/components/sharkiq/translations/hu.json new file mode 100644 index 00000000000..b765ad68a3f --- /dev/null +++ b/homeassistant/components/sharkiq/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/id.json b/homeassistant/components/sharkiq/translations/id.json new file mode 100644 index 00000000000..e3d8b4a8ed2 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/ru.json b/homeassistant/components/sharkiq/translations/ru.json index 60ce7d454d6..0e91c64c7d4 100644 --- a/homeassistant/components/sharkiq/translations/ru.json +++ b/homeassistant/components/sharkiq/translations/ru.json @@ -15,13 +15,13 @@ "reauth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index eff18064dcc..73f4093739a 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -1,7 +1,7 @@ """Data update coordinator for shark iq vacuums.""" +from __future__ import annotations import asyncio -from typing import Dict, List, Set from async_timeout import timeout from sharkiqpy import ( @@ -12,7 +12,7 @@ from sharkiqpy import ( SharkIqVacuum, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -27,11 +27,11 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): hass: HomeAssistant, config_entry: ConfigEntry, ayla_api: AylaApi, - shark_vacs: List[SharkIqVacuum], + shark_vacs: list[SharkIqVacuum], ) -> None: """Set up the SharkIqUpdateCoordinator class.""" self.ayla_api = ayla_api - self.shark_vacs: Dict[str, SharkIqVacuum] = { + self.shark_vacs: dict[str, SharkIqVacuum] = { sharkiq.serial_number: sharkiq for sharkiq in shark_vacs } self._config_entry = config_entry @@ -40,7 +40,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) @property - def online_dsns(self) -> Set[str]: + def online_dsns(self) -> set[str]: """Get the set of all online DSNs.""" return self._online_dsns @@ -76,7 +76,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): ) as err: _LOGGER.debug("Bad auth state. Attempting re-auth", exc_info=err) flow_context = { - "source": "reauth", + "source": SOURCE_REAUTH, "unique_id": self._config_entry.unique_id, } @@ -99,7 +99,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): _LOGGER.debug("Matching flow found") raise UpdateFailed(err) from err - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.exception("Unexpected error updating SharkIQ") raise UpdateFailed(err) from err diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 9684dde45e6..eed41fb1438 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -1,8 +1,8 @@ """Shark IQ Wrapper.""" - +from __future__ import annotations import logging -from typing import Dict, Iterable, Optional +from typing import Iterable from sharkiqpy import OperatingModes, PowerModes, Properties, SharkIqVacuum @@ -69,7 +69,7 @@ ATTR_RSSI = "rssi" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Shark IQ vacuum cleaner.""" coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - devices: Iterable["SharkIqVacuum"] = coordinator.shark_vacs.values() + devices: Iterable[SharkIqVacuum] = coordinator.shark_vacs.values() device_names = [d.name for d in devices] _LOGGER.debug( "Found %d Shark IQ device(s): %s", @@ -118,7 +118,7 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): return self.sharkiq.oem_model_number @property - def device_info(self) -> Dict: + def device_info(self) -> dict: """Device info dictionary.""" return { "identifiers": {(DOMAIN, self.serial_number)}, @@ -136,30 +136,30 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): return SUPPORT_SHARKIQ @property - def is_docked(self) -> Optional[bool]: + def is_docked(self) -> bool | None: """Is vacuum docked.""" return self.sharkiq.get_property_value(Properties.DOCKED_STATUS) @property - def error_code(self) -> Optional[int]: + def error_code(self) -> int | None: """Return the last observed error code (or None).""" return self.sharkiq.error_code @property - def error_message(self) -> Optional[str]: + def error_message(self) -> str | None: """Return the last observed error message (or None).""" if not self.error_code: return None return self.sharkiq.error_text @property - def operating_mode(self) -> Optional[str]: + def operating_mode(self) -> str | None: """Operating mode..""" op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) return OPERATING_STATE_MAP.get(op_mode) @property - def recharging_to_resume(self) -> Optional[int]: + def recharging_to_resume(self) -> int | None: """Return True if vacuum set to recharge and resume cleaning.""" return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) @@ -240,12 +240,12 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): # Various attributes we want to expose @property - def recharge_resume(self) -> Optional[bool]: + def recharge_resume(self) -> bool | None: """Recharge and resume mode active.""" return self.sharkiq.get_property_value(Properties.RECHARGE_RESUME) @property - def rssi(self) -> Optional[int]: + def rssi(self) -> int | None: """Get the WiFi RSSI.""" return self.sharkiq.get_property_value(Properties.RSSI) @@ -255,7 +255,7 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION) @property - def device_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Return a dictionary of device state attributes specific to sharkiq.""" data = { ATTR_ERROR_CODE: self.error_code, diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 1173d0477ab..089dc36b1a8 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -1,5 +1,6 @@ """Expose regular shell commands as services.""" import asyncio +from contextlib import suppress import logging import shlex @@ -87,10 +88,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT ) if process: - try: + with suppress(TypeError): await process.kill() - except TypeError: - pass del process return diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ccb52127525..be87e2556eb 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -75,6 +75,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): dev_reg = await device_registry.async_get_registry(hass) identifier = (DOMAIN, entry.unique_id) device_entry = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + if device_entry and entry.entry_id not in device_entry.config_entries: + device_entry = None sleep_period = entry.data.get("sleep_period") @@ -134,9 +136,9 @@ async def async_device_setup( ] = ShellyDeviceRestWrapper(hass, device) platforms = PLATFORMS - for component in platforms: + for platform in platforms: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) @@ -323,8 +325,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in platforms + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in platforms ] ) ) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 18220fc9e3a..385b3b30c36 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -47,7 +47,7 @@ SENSORS = { name="Gas", device_class=DEVICE_CLASS_GAS, value=lambda value: value in ["mild", "heavy"], - device_state_attributes=lambda block: {"detected": block.gas}, + extra_state_attributes=lambda block: {"detected": block.gas}, ), ("sensor", "smoke"): BlockAttributeDescription( name="Smoke", device_class=DEVICE_CLASS_SMOKE @@ -95,7 +95,7 @@ REST_SENSORS = { icon="mdi:update", value=lambda status, _: status["update"]["has_update"], default_enabled=False, - device_state_attributes=lambda status: { + extra_state_attributes=lambda status: { "latest_stable_version": status["update"]["new_version"], "installed_version": status["update"]["old_version"], }, diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index dfb078ee9c7..73c231086ef 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -16,8 +16,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import aiohttp_client -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC -from .const import DOMAIN # pylint:disable=unused-import +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, DOMAIN from .utils import get_coap_context, get_device_sleep_period _LOGGER = logging.getLogger(__name__) @@ -160,52 +159,34 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) self.host = zeroconf_info["host"] - if not info["auth"] and info.get("sleep_mode", False): - try: - self.device_info = await validate_input(self.hass, self.host, {}) - except HTTP_CONNECT_ERRORS: - return self.async_abort(reason="cannot_connect") - self.context["title_placeholders"] = { "name": zeroconf_info.get("name", "").split(".")[0] } + + if info["auth"]: + return await self.async_step_credentials() + + try: + self.device_info = await validate_input(self.hass, self.host, {}) + except HTTP_CONNECT_ERRORS: + return self.async_abort(reason="cannot_connect") + return await self.async_step_confirm_discovery() async def async_step_confirm_discovery(self, user_input=None): """Handle discovery confirm.""" errors = {} if user_input is not None: - if self.info["auth"]: - return await self.async_step_credentials() + return self.async_create_entry( + title=self.device_info["title"] or self.device_info["hostname"], + data={ + "host": self.host, + "sleep_period": self.device_info["sleep_period"], + "model": self.device_info["model"], + }, + ) - if self.device_info: - return self.async_create_entry( - title=self.device_info["title"] or self.device_info["hostname"], - data={ - "host": self.host, - "sleep_period": self.device_info["sleep_period"], - "model": self.device_info["model"], - }, - ) - - try: - device_info = await validate_input(self.hass, self.host, {}) - except HTTP_CONNECT_ERRORS: - errors["base"] = "cannot_connect" - except aioshelly.AuthRequired: - return await self.async_step_credentials() - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - return self.async_create_entry( - title=device_info["title"] or device_info["hostname"], - data={ - "host": self.host, - "sleep_period": device_info["sleep_period"], - "model": device_info["model"], - }, - ) + self._set_confirm_only() return self.async_show_form( step_id="confirm_discovery", diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 9d4851c92a4..b7cf1120949 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -1,5 +1,5 @@ """Provides device triggers for Shelly.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -60,7 +60,7 @@ async def async_validate_trigger_config(hass, config): ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Shelly devices.""" triggers = [] @@ -92,7 +92,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 71ab4703c79..48d37312225 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -1,7 +1,9 @@ """Shelly entity helper.""" +from __future__ import annotations + from dataclasses import dataclass import logging -from typing import Any, Callable, Optional, Union +from typing import Any, Callable import aioshelly @@ -142,17 +144,15 @@ class BlockAttributeDescription: name: str # Callable = lambda attr_info: unit - icon: Optional[str] = None - unit: Union[None, str, Callable[[dict], str]] = None + icon: str | None = None + unit: None | str | Callable[[dict], str] = None value: Callable[[Any], Any] = lambda val: val - device_class: Optional[str] = None + device_class: str | None = None default_enabled: bool = True - available: Optional[Callable[[aioshelly.Block], bool]] = None + available: Callable[[aioshelly.Block], bool] | None = None # Callable (settings, block), return true if entity should be removed - removal_condition: Optional[Callable[[dict, aioshelly.Block], bool]] = None - device_state_attributes: Optional[ - Callable[[aioshelly.Block], Optional[dict]] - ] = None + removal_condition: Callable[[dict, aioshelly.Block], bool] | None = None + extra_state_attributes: Callable[[aioshelly.Block], dict | None] | None = None @dataclass @@ -160,12 +160,12 @@ class RestAttributeDescription: """Class to describe a REST sensor.""" name: str - icon: Optional[str] = None - unit: Optional[str] = None + icon: str | None = None + unit: str | None = None value: Callable[[dict, Any], Any] = None - device_class: Optional[str] = None + device_class: str | None = None default_enabled: bool = True - device_state_attributes: Optional[Callable[[dict], Optional[dict]]] = None + extra_state_attributes: Callable[[dict], dict | None] | None = None class ShellyBlockEntity(entity.Entity): @@ -267,11 +267,6 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.description.value(value) - @property - def unit_of_measurement(self): - """Return unit of sensor.""" - return self._unit - @property def device_class(self): """Device class of sensor.""" @@ -293,12 +288,12 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.description.available(self.block) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - if self.description.device_state_attributes is None: + if self.description.extra_state_attributes is None: return None - return self.description.device_state_attributes(self.block) + return self.description.extra_state_attributes(self.block) class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): @@ -348,11 +343,6 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): ) return self._last_value - @property - def unit_of_measurement(self): - """Return unit of sensor.""" - return self.description.unit - @property def device_class(self): """Device class of sensor.""" @@ -369,12 +359,12 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return f"{self.wrapper.mac}-{self.attribute}" @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" - if self.description.device_state_attributes is None: + if self.description.extra_state_attributes is None: return None - return self.description.device_state_attributes(self.wrapper.device.status) + return self.description.extra_state_attributes(self.wrapper.device.status) class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEntity): @@ -387,7 +377,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti block: aioshelly.Block, attribute: str, description: BlockAttributeDescription, - entry: Optional[ConfigEntry] = None, + entry: ConfigEntry | None = None, ) -> None: """Initialize the sleeping sensor.""" self.last_state = None diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 0379bfec1cf..a9e13796875 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,5 +1,5 @@ """Light for Shelly.""" -from typing import Optional, Tuple +from __future__ import annotations from aioshelly import Block @@ -96,7 +96,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return self.block.output @property - def mode(self) -> Optional[str]: + def mode(self) -> str | None: """Return the color mode of the light.""" if self.mode_result: return self.mode_result["mode"] @@ -138,7 +138,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return int(white) @property - def hs_color(self) -> Tuple[float, float]: + def hs_color(self) -> tuple[float, float]: """Return the hue and saturation color value of light.""" if self.mode == "white": return color_RGB_to_hs(255, 255, 255) @@ -154,7 +154,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return color_RGB_to_hs(red, green, blue) @property - def color_temp(self) -> Optional[int]: + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" if self.mode == "color": return None diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index a757947c5cf..1ae274d6dfd 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.6.1"], + "requirements": ["aioshelly==0.6.2"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 472f3be4dae..b6d3bc2dbff 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,5 +1,6 @@ """Sensor for Shelly.""" from homeassistant.components import sensor +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEGREE, @@ -127,13 +128,10 @@ SENSORS = { ("sensor", "concentration"): BlockAttributeDescription( name="Gas Concentration", unit=CONCENTRATION_PARTS_PER_MILLION, - value=lambda value: value, icon="mdi:gauge", - # "sensorOp" is "normal" when the Shelly Gas is working properly and taking measurements. - available=lambda block: block.sensorOp == "normal", ), ("sensor", "extTemp"): BlockAttributeDescription( - name="External Temperature", + name="Temperature", unit=temperature_unit, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, @@ -159,7 +157,7 @@ SENSORS = { unit=PERCENTAGE, icon="mdi:progress-wrench", value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), - device_state_attributes=lambda block: { + extra_state_attributes=lambda block: { "Operational hours": round(block.totalWorkTime / 3600, 1) }, ), @@ -169,6 +167,12 @@ SENSORS = { value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_VOLTAGE, ), + ("sensor", "sensorOp"): BlockAttributeDescription( + name="Operation", + icon="mdi:cog-transfer", + value=lambda value: value, + extra_state_attributes=lambda block: {"self_test": block.selfTest}, + ), } REST_SENSORS = { @@ -203,7 +207,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class ShellySensor(ShellyBlockAttributeEntity): +class ShellySensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a shelly sensor.""" @property @@ -211,8 +215,13 @@ class ShellySensor(ShellyBlockAttributeEntity): """Return value of sensor.""" return self.attribute_value + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self._unit -class ShellyRestSensor(ShellyRestAttributeEntity): + +class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): """Represent a shelly REST sensor.""" @property @@ -220,8 +229,13 @@ class ShellyRestSensor(ShellyRestAttributeEntity): """Return value of sensor.""" return self.attribute_value + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self.description.unit -class ShellySleepingSensor(ShellySleepingBlockAttributeEntity): + +class ShellySleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity): """Represent a shelly sleeping sensor.""" @property @@ -231,3 +245,8 @@ class ShellySleepingSensor(ShellySleepingBlockAttributeEntity): return self.attribute_value return self.last_state + + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self._unit diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json new file mode 100644 index 00000000000..c856929a5e1 --- /dev/null +++ b/homeassistant/components/shelly/translations/bg.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_subtype": { + "button": "\u0411\u0443\u0442\u043e\u043d", + "button1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 4764936a41b..9d78d362c99 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "unsupported_firmware": "Das Ger\u00e4t verwendet eine nicht unterst\u00fctzte Firmware-Version." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -23,5 +24,21 @@ "description": "Vor der Einrichtung m\u00fcssen batteriebetriebene Ger\u00e4te durch Dr\u00fccken der Taste am Ger\u00e4t aufgeweckt werden." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Taste", + "button1": "Erste Taste", + "button2": "Zweite Taste", + "button3": "Dritte Taste" + }, + "trigger_type": { + "double": "{subtype} zweifach bet\u00e4tigt", + "long": "{subtype} gehalten", + "long_single": "{subtype} gehalten und dann einfach bet\u00e4tigt", + "single": "{subtype} einfach bet\u00e4tigt", + "single_long": "{subtype} einfach bet\u00e4tigt und dann gehalten", + "triple": "{subtype} dreifach bet\u00e4tigt" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 3b2d79a34a7..2c8f468aaed 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -1,7 +1,44 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "unsupported_firmware": "Az eszk\u00f6z nem t\u00e1mogatott firmware verzi\u00f3t haszn\u00e1l." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "credentials": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "host": "Hoszt" + }, + "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, most egy rajta l\u00e9v\u0151 gombbal fel\u00e9bresztheted az eszk\u00f6zt." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "Gomb", + "button1": "Els\u0151 gomb", + "button2": "M\u00e1sodik gomb", + "button3": "Harmadik gomb" + }, + "trigger_type": { + "double": "{subtype} dupla kattint\u00e1s", + "long": "{subtype} hosszan nyomva", + "long_single": "{subtype} hosszan nyomva, majd egy kattint\u00e1s", + "single": "{subtype} egy kattint\u00e1s", + "single_long": "{subtype} egy kattint\u00e1s, majd hosszan nyomva", + "triple": "{subtype} tripla kattint\u00e1s" } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json new file mode 100644 index 00000000000..606ee473805 --- /dev/null +++ b/homeassistant/components/shelly/translations/id.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "unsupported_firmware": "Perangkat menggunakan versi firmware yang tidak didukung." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Ingin menyiapkan {model} di {host}?\n\nPerangkat bertenaga baterai yang dilindungi kata sandi harus dibangunkan sebelum melanjutkan penyiapan.\nPerangkat bertenaga baterai yang tidak dilindungi kata sandi akan ditambahkan ketika perangkat bangun. Anda dapat membangunkan perangkat secara manual menggunakan tombol di atasnya sekarang juga atau menunggu pembaruan data berikutnya dari perangkat." + }, + "credentials": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Sebelum menyiapkan, perangkat bertenaga baterai harus dibangunkan. Anda dapat membangunkan perangkat menggunakan tombol di atasnya sekarang." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "Tombol", + "button1": "Tombol pertama", + "button2": "Tombol kedua", + "button3": "Tombol ketiga" + }, + "trigger_type": { + "double": "{subtype} diklik dua kali", + "long": "{subtype} diklik lama", + "long_single": "{subtype} diklik lama kemudian diklik sekali", + "single": "{subtype} diklik sekali", + "single_long": "{subtype} diklik sekali kemudian diklik lama", + "triple": "{subtype} diklik tiga kali" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ko.json b/homeassistant/components/shelly/translations/ko.json index 914c9a46bd8..d4af4248578 100644 --- a/homeassistant/components/shelly/translations/ko.json +++ b/homeassistant/components/shelly/translations/ko.json @@ -2,14 +2,18 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unsupported_firmware": "\uc774 \uc7a5\uce58\ub294 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4." + "unsupported_firmware": "\uae30\uae30\uac00 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud38c\uc6e8\uc5b4 \ubc84\uc804\uc744 \uc0ac\uc6a9\ud558\uace0 \uc788\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": "{name}", "step": { + "confirm_discovery": { + "description": "{host}\uc5d0\uc11c {model}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\ube44\ubc00\ubc88\ud638\ub85c \ubcf4\ud638\ub41c \ubc30\ud130\ub9ac \ubc29\uc2dd \uae30\uae30\ub294 \uc124\uc815\ud558\uae30 \uc804\uc5d0 \uc808\uc804 \ubaa8\ub4dc\ub97c \ud574\uc81c\ud574\uc57c \ud569\ub2c8\ub2e4.\n\ube44\ubc00\ubc88\ud638\ub85c \ubcf4\ud638\ub418\uc9c0 \uc54a\ub294 \ubc30\ud130\ub9ac \ubc29\uc2dd \uae30\uae30\ub294 \uae30\uae30\uc758 \uc808\uc804 \ubaa8\ub4dc\uac00 \ud574\uc81c\ub420 \ub54c \ucd94\uac00\ub418\uba70, \uae30\uae30\uc758 \ubc84\ud2bc\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc218\ub3d9\uc73c\ub85c \uae30\uae30\ub97c \uc808\uc804 \ud574\uc81c\uc2dc\ud0a4\uac70\ub098 \uae30\uae30\uc5d0\uc11c \ub2e4\uc74c \ub370\uc774\ud130\ub97c \uc5c5\ub370\uc774\ud2b8\ud560 \ub54c\uae4c\uc9c0 \uae30\ub2e4\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, "credentials": { "data": { "password": "\ube44\ubc00\ubc88\ud638", @@ -19,8 +23,25 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8" - } + }, + "description": "\uc124\uc815\ud558\uae30 \uc804\uc5d0 \ubc30\ud130\ub9ac\ub85c \uc791\ub3d9\ub418\ub294 \uae30\uae30\ub294 \uc808\uc804 \ubaa8\ub4dc\uac00 \ud574\uc81c\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uae30\uae30\uc758 \ubc84\ud2bc\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc808\uc804 \ubaa8\ub4dc\ub97c \ud574\uc81c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "\ubc84\ud2bc", + "button1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc", + "button2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "button3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc" + }, + "trigger_type": { + "double": "\"{subtype}\"\uc774(\uac00) \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c", + "long": "\"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\uc744 \ub54c", + "long_single": "\"{subtype}\"\uc774(\uac00) \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc9e7\uac8c \ub20c\ub838\uc744 \ub54c", + "single": "\"{subtype}\"\uc774(\uac00) \uc9e7\uac8c \ub20c\ub838\uc744 \ub54c", + "single_long": "\"{subtype}\"\uc774(\uac00) \uc9e7\uac8c \ub20c\ub838\ub2e4\uac00 \uae38\uac8c \ub20c\ub838\uc744 \ub54c", + "triple": "\"{subtype}\"\uc774(\uac00) \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index c486b9c6bfe..571fb9b4339 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "unsupported_firmware": "Het apparaat gebruikt een niet-ondersteunde firmwareversie." }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -22,7 +23,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Slapende (op batterij werkende) apparaten moeten wakker zijn wanneer deze apparaten opgezet worden. Je kunt deze apparaten nu wakker maken door op de knop erop te drukken" } } }, @@ -35,6 +37,9 @@ }, "trigger_type": { "double": "{subtype} dubbel geklikt", + "long": "{subtype} lang geklikt", + "long_single": "{subtype} lang geklikt en daarna \u00e9\u00e9n keer geklikt", + "single": "{subtype} enkel geklikt", "single_long": "{subtype} een keer geklikt en daarna lang geklikt", "triple": "{subtype} driemaal geklikt" } diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index a570cb7f9fb..9996e347e96 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -17,7 +17,7 @@ "credentials": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } }, "user": { diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 27152997ef7..126491f65c1 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -1,8 +1,8 @@ """Shelly helpers functions.""" +from __future__ import annotations from datetime import timedelta import logging -from typing import List, Optional, Tuple import aioshelly @@ -67,7 +67,7 @@ def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) -> def get_entity_name( device: aioshelly.Device, block: aioshelly.Block, - description: Optional[str] = None, + description: str | None = None, ) -> str: """Naming for switch and sensors.""" channel_name = get_device_channel_name(device, block) @@ -143,7 +143,7 @@ def get_device_uptime(status: dict, last_uptime: str) -> str: def get_input_triggers( device: aioshelly.Device, block: aioshelly.Block -) -> List[Tuple[str, str]]: +) -> list[tuple[str, str]]: """Return list of input triggers for block.""" if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids: return [] diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index d2a6a28fbe4..fa0fc2d3906 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -5,10 +5,9 @@ import logging import shodan import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([ShodanSensor(data, name)], True) -class ShodanSensor(Entity): +class ShodanSensor(SensorEntity): """Representation of the Shodan sensor.""" def __init__(self, data, name): @@ -78,7 +77,7 @@ class ShodanSensor(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index e438bf3b8f4..841865cd759 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -7,14 +7,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND +from homeassistant.const import ATTR_NAME, HTTP_BAD_REQUEST, HTTP_NOT_FOUND from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from .const import DOMAIN -ATTR_NAME = "name" ATTR_COMPLETE = "complete" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shopping_list/translations/hu.json b/homeassistant/components/shopping_list/translations/hu.json index 4a093bea379..5f092963da3 100644 --- a/homeassistant/components/shopping_list/translations/hu.json +++ b/homeassistant/components/shopping_list/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A bev\u00e1s\u00e1rl\u00f3lista m\u00e1r konfigur\u00e1lva van." + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "step": { "user": { diff --git a/homeassistant/components/shopping_list/translations/id.json b/homeassistant/components/shopping_list/translations/id.json new file mode 100644 index 00000000000..0efa42d0782 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "description": "Ingin mengonfigurasi daftar belanja?", + "title": "Daftar Belanja" + } + } + }, + "title": "Daftar Belanja" +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/nl.json b/homeassistant/components/shopping_list/translations/nl.json index de6045dd81b..e8de5fbae1d 100644 --- a/homeassistant/components/shopping_list/translations/nl.json +++ b/homeassistant/components/shopping_list/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "De Shopping List is al geconfigureerd." + "already_configured": "Service is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 277039b3ba6..fd5506ee513 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -7,7 +7,7 @@ import math from Adafruit_SHT31 import SHT31 import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, @@ -16,7 +16,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp from homeassistant.util import Throttle @@ -93,7 +92,7 @@ class SHTClient: self.humidity = humidity -class SHTSensor(Entity): +class SHTSensor(SensorEntity): """An abstract SHTSensor, can be either temperature or humidity.""" def __init__(self, sensor, name): diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 1de3cfeb8a0..75c2a4f0f63 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -7,10 +7,9 @@ from urllib.parse import urljoin import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, HTTP_OK, HTTP_UNAUTHORIZED import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -109,7 +108,7 @@ class SigfoxAPI: return self._devices -class SigfoxDevice(Entity): +class SigfoxDevice(SensorEntity): """Class for single sigfox device.""" def __init__(self, device_id, auth, name): @@ -155,6 +154,6 @@ class SigfoxDevice(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the last message.""" return self._message_data diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index e15fab1aaa3..fa636eb757f 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -170,7 +170,7 @@ class SighthoundEntity(ImageProcessingEntity): return ATTR_PEOPLE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes.""" if not self._last_detection: return {} diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index aa9519fd68b..0cab5b45b84 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,6 +2,6 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==8.1.1", "simplehound==0.3"], + "requirements": ["pillow==8.1.2", "simplehound==0.3"], "codeowners": ["@robmarkcole"] } diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 495ba29fefb..485284b3293 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -71,7 +71,7 @@ EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" DEFAULT_SOCKET_MIN_RETRY = 15 -SUPPORTED_PLATFORMS = ( +PLATFORMS = ( "alarm_control_panel", "binary_sensor", "lock", @@ -219,7 +219,7 @@ async def async_setup_entry(hass, config_entry): ) await simplisafe.async_init() - for platform in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) ) @@ -327,8 +327,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SUPPORTED_PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -634,7 +634,7 @@ class SimpliSafeEntity(CoordinatorEntity): return self._device_info @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index f17a2ce2e4c..09e0b96e742 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from . import async_get_client_id -from .const import DOMAIN, LOGGER # pylint: disable=unused-import +from .const import DOMAIN, LOGGER FULL_DATA_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 7e927ee942e..9f93a6f9e87 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -1,6 +1,7 @@ """Support for SimpliSafe freeze sensor.""" from simplipy.entity import EntityTypes +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT from homeassistant.core import callback @@ -25,7 +26,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors) -class SimplisafeFreezeSensor(SimpliSafeBaseSensor): +class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): """Define a SimpliSafe freeze sensor entity.""" def __init__(self, simplisafe, system, sensor): diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/simplisafe/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 7b989246de1..8a2deedc534 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -1,11 +1,23 @@ { "config": { + "abort": { + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, "error": { - "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van" + "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { + "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)", "password": "Jelsz\u00f3", "username": "E-mail" }, diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json new file mode 100644 index 00000000000..512d6a38405 --- /dev/null +++ b/homeassistant/components/simplisafe/translations/id.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Akun SimpliSafe ini sudah digunakan.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "identifier_exists": "Akun sudah terdaftar", + "invalid_auth": "Autentikasi tidak valid", + "still_awaiting_mfa": "Masih menunggu pengeklikan dari email MFA", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "mfa": { + "description": "Periksa email Anda untuk mendapatkan tautan dari SimpliSafe. Setelah memverifikasi tautan, kembali ke sini untuk menyelesaikan instalasi integrasi.", + "title": "Autentikasi Multi-Faktor SimpliSafe" + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Token akses Anda telah kedaluwarsa atau dicabut. Masukkan kata sandi Anda untuk menautkan kembali akun Anda.", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "code": "Kode (digunakan di antarmuka Home Assistant)", + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Isi informasi Anda." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code": "Kode (digunakan di antarmuka Home Assistant)" + }, + "title": "Konfigurasikan SimpliSafe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json index c5c1b057ea8..194fa6cecf4 100644 --- a/homeassistant/components/simplisafe/translations/ko.json +++ b/homeassistant/components/simplisafe/translations/ko.json @@ -7,14 +7,20 @@ "error": { "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "still_awaiting_mfa": "\uc544\uc9c1 \ub2e4\ub2e8\uacc4 \uc778\uc99d(MFA) \uc774\uba54\uc77c\uc758 \ub9c1\ud06c \ud074\ub9ad\uc744 \uae30\ub2e4\ub9ac\uace0\uc788\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "mfa": { + "description": "\uc774\uba54\uc77c\uc5d0\uc11c SimpliSafe\uc758 \ub9c1\ud06c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \ub9c1\ud06c\ub97c \ud655\uc778\ud55c \ud6c4 \uc5ec\uae30\ub85c \ub3cc\uc544\uc640 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc124\uce58\ub97c \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", + "title": "SimpliSafe \ub2e4\ub2e8\uacc4 \uc778\uc99d" + }, "reauth_confirm": { "data": { "password": "\ube44\ubc00\ubc88\ud638" }, - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ub9cc\ub8cc\ub418\uc5c8\uac70\ub098 \ud574\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uacc4\uc815\uc744 \ub2e4\uc2dc \uc5f0\uacb0\ud558\ub824\uba74 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index d3196c591cb..7a6d9c5c4e3 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -7,9 +7,14 @@ "error": { "identifier_exists": "Account bestaat al", "invalid_auth": "Ongeldige authenticatie", + "still_awaiting_mfa": "Wacht nog steeds op MFA-e-mailklik", "unknown": "Onverwachte fout" }, "step": { + "mfa": { + "description": "Controleer uw e-mail voor een link van SimpliSafe. Nadat u de link hebt geverifieerd, gaat u hier terug om de installatie van de integratie te voltooien.", + "title": "SimpliSafe Multi-Factor Authenticatie" + }, "reauth_confirm": { "data": { "password": "Wachtwoord" @@ -21,7 +26,7 @@ "data": { "code": "Code (gebruikt in Home Assistant)", "password": "Wachtwoord", - "username": "E-mailadres" + "username": "E-mail" }, "title": "Vul uw gegevens in" } diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index 7f484b712c1..3fe7aedfbb0 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -5,10 +5,9 @@ from random import Random import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util CONF_AMP = "amplitude" @@ -67,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([sensor], True) -class SimulatedSensor(Entity): +class SimulatedSensor(SensorEntity): """Class for simulated sensor.""" def __init__( @@ -137,7 +136,7 @@ class SimulatedSensor(Entity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" return { "amplitude": self._amp, diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 9b759327dca..3fdd2e55b0d 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -8,7 +8,7 @@ from pygatt.backends import Characteristic, GATTToolBackend from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_MAC, CONF_NAME, @@ -18,7 +18,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Skybeacon sensor.""" name = config.get(CONF_NAME) mac = config.get(CONF_MAC) - _LOGGER.debug("Setting up...") + _LOGGER.debug("Setting up") mon = Monitor(hass, mac, name) add_entities([SkybeaconTemp(name, mon)]) @@ -62,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): mon.start() -class SkybeaconHumid(Entity): +class SkybeaconHumid(SensorEntity): """Representation of a Skybeacon humidity sensor.""" def __init__(self, name, mon): @@ -86,12 +85,12 @@ class SkybeaconHumid(Entity): return PERCENTAGE @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_DEVICE: "SKYBEACON", ATTR_MODEL: 1} -class SkybeaconTemp(Entity): +class SkybeaconTemp(SensorEntity): """Representation of a Skybeacon temperature sensor.""" def __init__(self, name, mon): @@ -115,12 +114,12 @@ class SkybeaconTemp(Entity): return TEMP_CELSIUS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_DEVICE: "SKYBEACON", ATTR_MODEL: 1} -class Monitor(threading.Thread): +class Monitor(threading.Thread, SensorEntity): """Connection handling.""" def __init__(self, hass, mac, name): diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index c1c9d76314c..2acb729d767 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -82,7 +82,7 @@ class SkybellDevice(Entity): self._device.refresh() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 8949a58fa01..7e075fba38a 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -76,9 +76,9 @@ class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): return self._device_class @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - attrs = super().device_state_attributes + attrs = super().extra_state_attributes attrs["event_date"] = self._event.get("createdAt") diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 09a7400a035..8dc13814c67 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class SkybellSensor(SkybellDevice): +class SkybellSensor(SkybellDevice, SensorEntity): """A sensor implementation for Skybell devices.""" def __init__(self, device, sensor_type): diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 985f59a6715..f1e293773bd 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import logging import os -from typing import Any, List, Optional, TypedDict +from typing import Any, TypedDict from urllib.parse import urlparse from aiohttp import BasicAuth, FormData @@ -20,7 +20,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME +from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_ICON, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.template as template @@ -35,7 +35,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_BLOCKS = "blocks" ATTR_BLOCKS_TEMPLATE = "blocks_template" ATTR_FILE = "file" -ATTR_ICON = "icon" ATTR_PASSWORD = "password" ATTR_PATH = "path" ATTR_URL = "url" @@ -106,14 +105,14 @@ class MessageT(TypedDict, total=False): username: str # Optional key icon_url: str # Optional key icon_emoji: str # Optional key - blocks: List[Any] # Optional key + blocks: list[Any] # Optional key async def async_get_service( hass: HomeAssistantType, config: ConfigType, - discovery_info: Optional[DiscoveryInfoType] = None, -) -> Optional[SlackNotificationService]: + discovery_info: DiscoveryInfoType | None = None, +) -> SlackNotificationService | None: """Set up the Slack notification service.""" session = aiohttp_client.async_get_clientsession(hass) client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session) @@ -147,7 +146,7 @@ def _async_get_filename_from_url(url: str) -> str: @callback -def _async_sanitize_channel_names(channel_list: List[str]) -> List[str]: +def _async_sanitize_channel_names(channel_list: list[str]) -> list[str]: """Remove any # symbols from a channel list.""" return [channel.lstrip("#") for channel in channel_list] @@ -174,8 +173,8 @@ class SlackNotificationService(BaseNotificationService): hass: HomeAssistantType, client: WebClient, default_channel: str, - username: Optional[str], - icon: Optional[str], + username: str | None, + icon: str | None, ) -> None: """Initialize.""" self._client = client @@ -187,9 +186,9 @@ class SlackNotificationService(BaseNotificationService): async def _async_send_local_file_message( self, path: str, - targets: List[str], + targets: list[str], message: str, - title: Optional[str], + title: str | None, ) -> None: """Upload a local file (with message) to Slack.""" if not self._hass.config.is_allowed_path(path): @@ -213,12 +212,12 @@ class SlackNotificationService(BaseNotificationService): async def _async_send_remote_file_message( self, url: str, - targets: List[str], + targets: list[str], message: str, - title: Optional[str], + title: str | None, *, - username: Optional[str] = None, - password: Optional[str] = None, + username: str | None = None, + password: str | None = None, ) -> None: """Upload a remote file (with message) to Slack. @@ -263,13 +262,13 @@ class SlackNotificationService(BaseNotificationService): async def _async_send_text_only_message( self, - targets: List[str], + targets: list[str], message: str, - title: Optional[str], + title: str | None, *, - username: Optional[str] = None, - icon: Optional[str] = None, - blocks: Optional[Any] = None, + username: str | None = None, + icon: str | None = None, + blocks: Any | None = None, ) -> None: """Send a text-only message.""" message_dict: MessageT = {"link_names": True, "text": message} diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index ae48c059bb8..8f5c17dad89 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -1,4 +1,6 @@ """Support for SleepIQ sensors.""" +from homeassistant.components.sensor import SensorEntity + from . import SleepIQSensor from .const import DOMAIN, SENSOR_TYPES, SIDES, SLEEP_NUMBER @@ -21,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class SleepNumberSensor(SleepIQSensor): +class SleepNumberSensor(SleepIQSensor, SensorEntity): """Implementation of a SleepIQ sensor.""" def __init__(self, sleepiq_data, bed_id, side): diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 470cf9e5a1f..9e925af3391 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -55,7 +55,7 @@ class SlideCover(CoverEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return {ATTR_ID: self._id} diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 94bab40a3b7..2290f3a330f 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -5,7 +5,7 @@ import logging import pysma import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -19,7 +19,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.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -169,7 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_track_time_interval(hass, async_sma, interval) -class SMAsensor(Entity): +class SMAsensor(SensorEntity): """Representation of a SMA sensor.""" def __init__(self, pysma_sensor, sub_sensors): @@ -196,7 +195,7 @@ class SMAsensor(Entity): return self._sensor.unit @property - def device_state_attributes(self): # Can be remove from 0.99 + def extra_state_attributes(self): # Can be remove from 0.99 """Return the state attributes of the sensor.""" return self._attr diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index aed67c5c167..f803f38b8ea 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -21,7 +21,7 @@ from .const import ( CONF_SERIALNUMBER, DOMAIN, MIN_TIME_BETWEEN_UPDATES, - SMAPPEE_PLATFORMS, + PLATFORMS, TOKEN_URL, ) @@ -92,9 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee) - for component in SMAPPEE_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -105,8 +105,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SMAPPEE_PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index 2c69f1ccb96..fc059509ced 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -12,7 +12,7 @@ CONF_TITLE = "title" ENV_CLOUD = "cloud" ENV_LOCAL = "local" -SMAPPEE_PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "sensor", "switch"] SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2") diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 41041c1a002..43483dbdb1e 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -1,6 +1,6 @@ """Support for monitoring a Smappee energy sensor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_WATT_HOUR, POWER_WATT, VOLT -from homeassistant.helpers.entity import Entity from .const import DOMAIN @@ -239,7 +239,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class SmappeeSensor(Entity): +class SmappeeSensor(SensorEntity): """Implementation of a Smappee sensor.""" def __init__(self, smappee_base, service_location, sensor, attributes): diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 4258cfb0912..15bfd4dc5d2 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -2,7 +2,21 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." + }, + "flow_title": "Smappee: {name}", + "step": { + "local": { + "data": { + "host": "Hoszt" + } + }, + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/id.json b/homeassistant/components/smappee/translations/id.json new file mode 100644 index 00000000000..b72200c34ca --- /dev/null +++ b/homeassistant/components/smappee/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "already_configured_local_device": "Perangkat lokal sudah dikonfigurasi. Hapus perangkat tersebut terlebih dahulu sebelum mengonfigurasi perangkat awan.", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "cannot_connect": "Gagal terhubung", + "invalid_mdns": "Perangkat tidak didukung untuk integrasi Smappee.", + "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})" + }, + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "data": { + "environment": "Lingkungan" + }, + "description": "Siapkan Smappee Anda untuk diintegrasikan dengan Home Assistant." + }, + "local": { + "data": { + "host": "Host" + }, + "description": "Masukkan host untuk memulai integrasi lokal Smappee" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "zeroconf_confirm": { + "description": "Ingin menambahkan perangkat Smappee dengan nomor seri `{serialnumber}` ke Home Assistant?", + "title": "Peranti Smappee yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json index 8509b65ca09..b2f0e5880c5 100644 --- a/homeassistant/components/smappee/translations/ko.json +++ b/homeassistant/components/smappee/translations/ko.json @@ -2,19 +2,33 @@ "config": { "abort": { "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured_local_device": "\ub85c\uceec \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \ud074\ub77c\uc6b0\ub4dc \uae30\uae30\ub97c \uad6c\uc131\ud558\uae30 \uc804\uc5d0 \uc774\ub7ec\ud55c \uae30\uae30\ub97c \uba3c\uc800 \uc81c\uac70\ud574\uc8fc\uc138\uc694.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_mdns": "Smappee \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 \uae30\uae30\uc785\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.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "\ud658\uacbd" + }, + "description": "Home Assistant\uc5d0 Smappee \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4." + }, "local": { "data": { "host": "\ud638\uc2a4\ud2b8" - } + }, + "description": "Smappee \ub85c\uceec \uc5f0\ub3d9\uc744 \uc2dc\uc791\ud560 \ud638\uc2a4\ud2b8\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" }, "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "zeroconf_confirm": { + "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serialnumber}`\uc758 Smappee \uae30\uae30\ub97c Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c Smpappee \uae30\uae30" } } } diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json index 10a4fe2efab..66ede5e8c14 100644 --- a/homeassistant/components/smappee/translations/nl.json +++ b/homeassistant/components/smappee/translations/nl.json @@ -9,7 +9,14 @@ "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "Omgeving" + }, + "description": "Stel uw Smappee in om te integreren met Home Assistant." + }, "local": { "data": { "host": "Host" @@ -18,6 +25,10 @@ }, "pick_implementation": { "title": "Kies een authenticatie methode" + }, + "zeroconf_confirm": { + "description": "Wilt u het Smappee apparaat met serienummer `{serialnumber}` toevoegen aan Home Assistant?", + "title": "Ontdekt Smappee apparaat" } } } diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 7b1c6cfa9b7..3180248fcd1 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -83,9 +83,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): asyncio.create_task(coordinator.async_refresh()) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -122,8 +122,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/smart_meter_texas/config_flow.py b/homeassistant/components/smart_meter_texas/config_flow.py index 211957dac9d..9f6df058cc7 100644 --- a/homeassistant/components/smart_meter_texas/config_flow.py +++ b/homeassistant/components/smart_meter_texas/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index e65fbdcb531..54e5969f8ea 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -1,6 +1,7 @@ """Support for Smart Meter Texas sensors.""" from smart_meter_texas import Meter +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_ADDRESS, ENERGY_KILO_WATT_HOUR from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity @@ -29,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity): +class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Representation of an Smart Meter Texas sensor.""" def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator): @@ -65,7 +66,7 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attributes = { METER_NUMBER: self.meter.meter, diff --git a/homeassistant/components/smart_meter_texas/translations/hu.json b/homeassistant/components/smart_meter_texas/translations/hu.json index 3b2d79a34a7..fd8db27da5e 100644 --- a/homeassistant/components/smart_meter_texas/translations/hu.json +++ b/homeassistant/components/smart_meter_texas/translations/hu.json @@ -2,6 +2,19 @@ "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" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/id.json b/homeassistant/components/smart_meter_texas/translations/id.json new file mode 100644 index 00000000000..4a84db42a14 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/id.json @@ -0,0 +1,20 @@ +{ + "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": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/ru.json b/homeassistant/components/smart_meter_texas/translations/ru.json index 3f4677a050e..aef0fdff54e 100644 --- a/homeassistant/components/smart_meter_texas/translations/ru.json +++ b/homeassistant/components/smart_meter_texas/translations/ru.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index c826b5d8f4d..c259ef71aab 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.typing import HomeAssistantType DOMAIN = "smarthab" DATA_HUB = "hub" -COMPONENTS = ["light", "cover"] +PLATFORMS = ["light", "cover"] _LOGGER = logging.getLogger(__name__) @@ -69,9 +69,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): # Pass hub object to child platforms hass.data[DOMAIN][entry.entry_id] = {DATA_HUB: hub} - for component in COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -83,8 +83,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): result = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/smarthab/config_flow.py b/homeassistant/components/smarthab/config_flow.py index 68f0460a4cc..9a4ec3ef325 100644 --- a/homeassistant/components/smarthab/config_flow.py +++ b/homeassistant/components/smarthab/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -# pylint: disable=unused-import from . import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json index b40828cc764..222c95bba16 100644 --- a/homeassistant/components/smarthab/translations/hu.json +++ b/homeassistant/components/smarthab/translations/hu.json @@ -1,7 +1,15 @@ { "config": { + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + }, "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet." } } diff --git a/homeassistant/components/smarthab/translations/id.json b/homeassistant/components/smarthab/translations/id.json new file mode 100644 index 00000000000..7a776eac304 --- /dev/null +++ b/homeassistant/components/smarthab/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentikasi tidak valid", + "service": "Terjadi kesalahan saat mencoba menjangkau SmartHab. Layanan mungkin sedang mengalami gangguan. Periksa koneksi Anda.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + }, + "description": "Untuk alasan teknis, pastikan untuk menggunakan akun sekunder khusus untuk penyiapan Home Assistant Anda. Anda dapat membuatnya dari aplikasi SmartHab.", + "title": "Siapkan SmartHab" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ko.json b/homeassistant/components/smarthab/translations/ko.json index d39931cbc03..1641555b412 100644 --- a/homeassistant/components/smarthab/translations/ko.json +++ b/homeassistant/components/smarthab/translations/ko.json @@ -11,7 +11,7 @@ "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "\uae30\uc220\uc801\uc778 \uc774\uc720\ub85c Home Assistant \uc124\uc815\uacfc \uad00\ub828\ub41c \ubcf4\uc870 \uacc4\uc815\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. SmartHab \uc751\uc6a9 \ud504\ub85c\uadf8\ub7a8\uc5d0\uc11c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\uae30\uc220\uc801\uc778 \uc774\uc720\ub85c Home Assistant \uc124\uc815\uacfc \uad00\ub828\ub41c \ubcf4\uc870 \uacc4\uc815\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. SmartHab \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "SmartHab \uc124\uce58\ud558\uae30" } } diff --git a/homeassistant/components/smarthab/translations/nl.json b/homeassistant/components/smarthab/translations/nl.json index 7f5fc7fe27c..31a02ae2b97 100644 --- a/homeassistant/components/smarthab/translations/nl.json +++ b/homeassistant/components/smarthab/translations/nl.json @@ -10,7 +10,9 @@ "data": { "email": "E-mail", "password": "Wachtwoord" - } + }, + "description": "Om technische redenen moet u een tweede account gebruiken dat specifiek is voor uw Home Assistant-installatie. U kunt er een aanmaken vanuit de SmartHab-toepassing.", + "title": "Stel SmartHab in" } } } diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index d184a3ca6ce..77ef913c629 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -36,8 +36,8 @@ from .const import ( DATA_MANAGER, DOMAIN, EVENT_BUTTON, + PLATFORMS, SIGNAL_SMARTTHINGS_UPDATE, - SUPPORTED_PLATFORMS, TOKEN_REFRESH_INTERVAL, ) from .smartapp import ( @@ -184,9 +184,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): ) return False - for component in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -213,8 +213,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): broker.disconnect() tasks = [ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SUPPORTED_PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] return all(await asyncio.gather(*tasks)) @@ -293,11 +293,13 @@ class DeviceBroker: for device in devices: capabilities = device.capabilities.copy() slots = {} - for platform_name in SUPPORTED_PLATFORMS: - platform = importlib.import_module(f".{platform_name}", self.__module__) - if not hasattr(platform, "get_capabilities"): + for platform in PLATFORMS: + platform_module = importlib.import_module( + f".{platform}", self.__module__ + ) + if not hasattr(platform_module, "get_capabilities"): continue - assigned = platform.get_capabilities(capabilities) + assigned = platform_module.get_capabilities(capabilities) if not assigned: continue # Draw-down capabilities and set slot assignment @@ -305,7 +307,7 @@ class DeviceBroker: if capability not in capabilities: continue capabilities.remove(capability) - slots[capability] = platform_name + slots[capability] = platform assignments[device.device_id] = slots return assignments diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 41e915d5c95..dd4c1e2928c 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -1,5 +1,7 @@ """Support for binary sensors through the SmartThings cloud API.""" -from typing import Optional, Sequence +from __future__ import annotations + +from typing import Sequence from pysmartthings import Attribute, Capability @@ -52,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" return [ capability for capability in CAPABILITY_TO_ATTRIB if capability in capabilities diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 6ce872cdac7..76c168fbc38 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,8 +1,10 @@ """Support for climate devices through the SmartThings cloud API.""" +from __future__ import annotations + import asyncio from collections.abc import Iterable import logging -from typing import Optional, Sequence +from typing import Sequence from pysmartthings import Attribute, Capability @@ -103,7 +105,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" supported = [ Capability.air_conditioner_mode, @@ -274,7 +276,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): return self._device.status.supported_thermostat_fan_modes @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" return OPERATING_STATE_TO_ACTION.get( self._device.status.thermostat_operating_state @@ -415,7 +417,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): return self._device.status.temperature @property - def device_state_attributes(self): + def extra_state_attributes(self): """ Return device specific state attributes. diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index e320c335a08..f6cca7e0276 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession -# pylint: disable=unused-import from .const import ( APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 03188411f07..a7aa9066dd2 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -31,7 +31,7 @@ STORAGE_VERSION = 1 # Ordered 'specific to least-specific platform' in order for capabilities # to be drawn-down and represented by the most appropriate platform. -SUPPORTED_PLATFORMS = [ +PLATFORMS = [ "climate", "fan", "light", diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index ddc52ec3f6c..8fff4ebbdfa 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -1,5 +1,7 @@ """Support for covers through the SmartThings cloud API.""" -from typing import Optional, Sequence +from __future__ import annotations + +from typing import Sequence from pysmartthings import Attribute, Capability @@ -46,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" min_required = [ Capability.door_control, @@ -147,7 +149,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): return self._device_class @property - def device_state_attributes(self): + def extra_state_attributes(self): """Get additional state attributes.""" return self._state_attrs diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 4cd451e2416..167f3a38edf 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -1,6 +1,8 @@ """Support for fans through the SmartThings cloud API.""" +from __future__ import annotations + import math -from typing import Optional, Sequence +from typing import Sequence from pysmartthings import Capability @@ -29,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" supported = [Capability.switch, Capability.fan_speed] # Must have switch and fan_speed @@ -76,7 +78,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): return self._device.status.switch @property - def percentage(self) -> str: + def percentage(self) -> int: """Return the current speed percentage.""" return ranged_value_to_percentage(SPEED_RANGE, self._device.status.fan_speed) diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 1e4161abd0f..de678f255fa 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -1,6 +1,8 @@ """Support for lights through the SmartThings cloud API.""" +from __future__ import annotations + import asyncio -from typing import Optional, Sequence +from typing import Sequence from pysmartthings import Capability @@ -34,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" supported = [ Capability.switch, diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index d6b615b47a7..2cd0b283cca 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -1,5 +1,7 @@ """Support for locks through the SmartThings cloud API.""" -from typing import Optional, Sequence +from __future__ import annotations + +from typing import Sequence from pysmartthings import Attribute, Capability @@ -31,7 +33,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" if Capability.lock in capabilities: return [Capability.lock] @@ -57,7 +59,7 @@ class SmartThingsLock(SmartThingsEntity, LockEntity): return self._device.status.lock == ST_STATE_LOCKED @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" state_attrs = {} status = self._device.status.attributes[Attribute.lock] diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py index 11ee6dc83e1..e3d93c663fa 100644 --- a/homeassistant/components/smartthings/scene.py +++ b/homeassistant/components/smartthings/scene.py @@ -24,7 +24,7 @@ class SmartThingsScene(Scene): await self._scene.execute() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Get attributes about the state.""" return { "icon": self._scene.icon, diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 835c4168f07..86377e32e23 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -1,9 +1,12 @@ """Support for sensors through the SmartThings cloud API.""" +from __future__ import annotations + from collections import namedtuple -from typing import Optional, Sequence +from typing import Sequence from pysmartthings import Attribute, Capability +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( AREA_SQUARE_METERS, CONCENTRATION_PARTS_PER_MILLION, @@ -297,14 +300,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" return [ capability for capability in CAPABILITY_TO_SENSORS if capability in capabilities ] -class SmartThingsSensor(SmartThingsEntity): +class SmartThingsSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Sensor.""" def __init__( @@ -344,7 +347,7 @@ class SmartThingsSensor(SmartThingsEntity): return UNITS.get(unit, unit) if unit else self._default_unit -class SmartThingsThreeAxisSensor(SmartThingsEntity): +class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): """Define a SmartThings Three Axis Sensor.""" def __init__(self, device, index): diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index ff70648ddcf..d8bcd455415 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -1,5 +1,7 @@ """Support for switches through the SmartThings cloud API.""" -from typing import Optional, Sequence +from __future__ import annotations + +from typing import Sequence from pysmartthings import Attribute, Capability @@ -21,7 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: +def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: """Return all capabilities supported if minimum required are present.""" # Must be able to be turned on/off. if Capability.switch in capabilities: diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 5cbe1d086bc..17c0a1a1b04 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -12,11 +12,15 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent] ( {token_url} ), amelyet az [utas\u00edt\u00e1sok] ( {component_url} ) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban." + "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban." + }, + "select_location": { + "data": { + "location_id": "Elhelyezked\u00e9s" + } }, "user": { - "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k] ({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", - "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" + "title": "Callback URL meger\u0151s\u00edt\u00e9se" } } } diff --git a/homeassistant/components/smartthings/translations/id.json b/homeassistant/components/smartthings/translations/id.json new file mode 100644 index 00000000000..77846487d85 --- /dev/null +++ b/homeassistant/components/smartthings/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "invalid_webhook_url": "Home Assistant tidak dikonfigurasi dengan benar untuk menerima pembaruan dari SmartThings. URL webhook tidak valid:\n> {webhook_url}\n\nPerbarui konfigurasi Anda sesuai [petunjuk]({component_url}), kemudian mulai ulang Home Assistant, dan coba kembali.", + "no_available_locations": "Tidak ada SmartThings Location untuk disiapkan di Home Assistant." + }, + "error": { + "app_setup_error": "Tidak dapat menyiapkan SmartApp. Coba lagi.", + "token_forbidden": "Token tidak memiliki cakupan OAuth yang diperlukan.", + "token_invalid_format": "Token harus dalam format UID/GUID", + "token_unauthorized": "Token tidak valid atau tidak lagi diotorisasi.", + "webhook_error": "SmartThings tidak dapat memvalidasi URL webhook. Pastikan URL webhook dapat dijangkau dari internet, lalu coba lagi." + }, + "step": { + "authorize": { + "title": "Otorisasi Home Assistant" + }, + "pat": { + "data": { + "access_token": "Token Akses" + }, + "description": "Masukkan [Token Akses Pribadi]({token_url}) SmartThings yang telah dibuat sesuai [petunjuk]({component_url}). Ini akan digunakan untuk membuat integrasi Home Assistant dalam akun SmartThings Anda.", + "title": "Masukkan Token Akses Pribadi" + }, + "select_location": { + "data": { + "location_id": "Lokasi" + }, + "description": "Pilih SmartThings Location yang ingin ditambahkan ke Home Assistant. Kami akan membuka jendela baru dan meminta Anda untuk masuk dan mengotorisasi instalasi integrasi Home Assistant ke Location yang dipilih.", + "title": "Pilih Location" + }, + "user": { + "description": "SmartThings akan dikonfigurasi untuk mengirim pembaruan push ke Home Assistant di:\n > {webhook_url} \n\nJika ini tidak benar, perbarui konfigurasi Anda, mulai ulang Home Assistant, dan coba lagi.", + "title": "Konfirmasikan URL Panggilan Balik" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/ko.json b/homeassistant/components/smartthings/translations/ko.json index 0ee28136651..dc02f25451b 100644 --- a/homeassistant/components/smartthings/translations/ko.json +++ b/homeassistant/components/smartthings/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant \uac00 SmartThings \uc5d0\uc11c \uc5c5\ub370\uc774\ud2b8\ub97c \uc218\uc2e0\ud558\ub3c4\ub85d \uc62c\ubc14\ub974\uac8c \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc6f9 \ud6c5 URL \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4:\n> {webhook_url} \n\n[\uc548\ub0b4]({component_url}) \ub97c \ucc38\uace0\ud558\uc5ec \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "no_available_locations": "Home Assistant \uc5d0\uc11c \uc124\uc815\ud560 \uc218 \uc788\ub294 SmartThings \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + "invalid_webhook_url": "Home Assistant\uac00 SmartThings\uc5d0\uc11c \uc5c5\ub370\uc774\ud2b8\ub97c \uc218\uc2e0\ud558\ub3c4\ub85d \uc62c\ubc14\ub974\uac8c \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc6f9 \ud6c5 URL\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4:\n> {webhook_url} \n\n[\uc548\ub0b4]({component_url})\ub97c \ucc38\uace0\ud558\uc5ec \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "no_available_locations": "Home Assistant\uc5d0\uc11c \uc124\uc815\ud560 \uc218 \uc788\ub294 SmartThings \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "error": { "app_setup_error": "SmartApp \uc744 \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", @@ -19,18 +19,18 @@ "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" }, - "description": "[\uc548\ub0b4]({component_url}) \uc5d0 \ub530\ub77c \uc0dd\uc131\ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url}) \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. SmartThings \uacc4\uc815\uc5d0\uc11c Home Assistant \uc5f0\ub3d9\uc744 \ub9cc\ub4dc\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4.", - "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070 \uc785\ub825\ud558\uae30" + "description": "[\uc548\ub0b4]({component_url})\uc5d0 \ub530\ub77c \uc0dd\uc131\ub41c SmartThings [\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070]({token_url})\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. SmartThings \uacc4\uc815\uc5d0\uc11c Home Assistant \uc5f0\ub3d9\uc744 \ub9cc\ub4dc\ub294\ub370 \uc0ac\uc6a9\ub429\ub2c8\ub2e4.", + "title": "\uac1c\uc778 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" }, "select_location": { "data": { "location_id": "\uc704\uce58" }, - "description": "Home Assistant \uc5d0 \ucd94\uac00\ud558\ub824\ub294 SmartThings \uc704\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc0c8\ub86d\uac8c \uc5f4\ub9b0 \ub85c\uadf8\uc778 \ucc3d\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\uba74 \uc120\ud0dd\ud55c \uc704\uce58\uc5d0 Home Assistant \uc5f0\ub3d9\uc744 \uc2b9\uc778\ud558\ub77c\ub294 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", + "description": "Home Assistant\uc5d0 \ucd94\uac00\ud558\ub824\ub294 SmartThings \uc704\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc0c8\ub86d\uac8c \uc5f4\ub9b0 \ub85c\uadf8\uc778 \ucc3d\uc5d0\uc11c \ub85c\uadf8\uc778\uc744 \ud558\uba74 \uc120\ud0dd\ud55c \uc704\uce58\uc5d0 Home Assistant \uc5f0\ub3d9\uc744 \uc2b9\uc778\ud558\ub77c\ub294 \uba54\uc2dc\uc9c0\uac00 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", "title": "\uc704\uce58 \uc120\ud0dd\ud558\uae30" }, "user": { - "description": "SmartThings \ub294 \uc544\ub798\uc758 \uc6f9 \ud6c5 \uc8fc\uc18c\ub85c Home Assistant \uc5d0 \ud478\uc2dc \uc5c5\ub370\uc774\ud2b8\ub97c \ubcf4\ub0b4\ub3c4\ub85d \uad6c\uc131\ub429\ub2c8\ub2e4. \n > {webhook_url} \n\n\uc774 \uad6c\uc131\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc73c\uba74 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant \ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "description": "SmartThings\ub294 \uc544\ub798\uc758 \uc6f9 \ud6c5 \uc8fc\uc18c\ub85c Home Assistant\uc5d0 \ud478\uc2dc \uc5c5\ub370\uc774\ud2b8\ub97c \ubcf4\ub0b4\ub3c4\ub85d \uad6c\uc131\ub429\ub2c8\ub2e4. \n > {webhook_url} \n\n\uc774 \uad6c\uc131\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\ub2e4\uba74 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud558\uace0 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "title": "\ucf5c\ubc31 URL \ud655\uc778\ud558\uae30" } } diff --git a/homeassistant/components/smartthings/translations/nl.json b/homeassistant/components/smartthings/translations/nl.json index a77ad40f0ca..6c141b624d2 100644 --- a/homeassistant/components/smartthings/translations/nl.json +++ b/homeassistant/components/smartthings/translations/nl.json @@ -9,7 +9,7 @@ "token_forbidden": "Het token heeft niet de vereiste OAuth-scopes.", "token_invalid_format": "Het token moet de UID/GUID-indeling hebben", "token_unauthorized": "Het token is ongeldig of niet langer geautoriseerd.", - "webhook_error": "SmartThings kon het in 'base_url` geconfigureerde endpoint niet goedkeuren. Lees de componentvereisten door." + "webhook_error": "SmartThings kan de webhook URL niet valideren. Zorg ervoor dat de webhook URL bereikbaar is vanaf het internet en probeer het opnieuw." }, "step": { "authorize": { @@ -30,8 +30,8 @@ "title": "Locatie selecteren" }, "user": { - "description": "Voer een SmartThings [Personal Access Token]({token_url}) in die is aangemaakt volgens de [instructies]({component_url}).", - "title": "Persoonlijk toegangstoken invoeren" + "description": "SmartThings zal worden geconfigureerd om push updates te sturen naar Home Assistant op:\n> {webhook_url}\n\nAls dit niet correct is, werk dan uw configuratie bij, start Home Assistant opnieuw op en probeer het opnieuw.", + "title": "Bevestig Callback URL" } } } diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 52dbfd71a37..bbeece36655 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -1,23 +1,35 @@ """Platform for binary sensor integration.""" import logging +from smarttub import SpaReminder + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) -from .const import DOMAIN, SMARTTUB_CONTROLLER -from .entity import SmartTubSensorBase +from .const import ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER +from .entity import SmartTubEntity, SmartTubSensorBase _LOGGER = logging.getLogger(__name__) +# whether the reminder has been snoozed (bool) +ATTR_REMINDER_SNOOZED = "snoozed" + async def async_setup_entry(hass, entry, async_add_entities): """Set up binary sensor entities for the binary sensors in the tub.""" controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] - entities = [SmartTubOnline(controller.coordinator, spa) for spa in controller.spas] + entities = [] + for spa in controller.spas: + entities.append(SmartTubOnline(controller.coordinator, spa)) + entities.extend( + SmartTubReminder(controller.coordinator, spa, reminder) + for reminder in controller.coordinator.data[spa.id][ATTR_REMINDERS].values() + ) async_add_entities(entities) @@ -38,3 +50,43 @@ class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): def device_class(self) -> str: """Return the device class for this entity.""" return DEVICE_CLASS_CONNECTIVITY + + +class SmartTubReminder(SmartTubEntity, BinarySensorEntity): + """Reminders for maintenance actions.""" + + def __init__(self, coordinator, spa, reminder): + """Initialize the entity.""" + super().__init__( + coordinator, + spa, + f"{reminder.name.title()} Reminder", + ) + self.reminder_id = reminder.id + + @property + def unique_id(self): + """Return a unique id for this sensor.""" + return f"{self.spa.id}-reminder-{self.reminder_id}" + + @property + def reminder(self) -> SpaReminder: + """Return the underlying SpaReminder object for this entity.""" + return self.coordinator.data[self.spa.id]["reminders"][self.reminder_id] + + @property + def is_on(self) -> bool: + """Return whether the specified maintenance action needs to be taken.""" + return self.reminder.remaining_days == 0 + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_REMINDER_SNOOZED: self.reminder.snoozed, + } + + @property + def device_class(self) -> str: + """Return the device class for this entity.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 66c03a22e1f..be564c84a94 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -23,6 +23,19 @@ _LOGGER = logging.getLogger(__name__) PRESET_DAY = "day" +PRESET_MODES = { + Spa.HeatMode.AUTO: PRESET_NONE, + Spa.HeatMode.ECONOMY: PRESET_ECO, + Spa.HeatMode.DAY: PRESET_DAY, +} + +HEAT_MODES = {v: k for k, v in PRESET_MODES.items()} + +HVAC_ACTIONS = { + "OFF": CURRENT_HVAC_IDLE, + "ON": CURRENT_HVAC_HEAT, +} + async def async_setup_entry(hass, entry, async_add_entities): """Set up climate entity for the thermostat in the tub.""" @@ -39,22 +52,9 @@ async def async_setup_entry(hass, entry, async_add_entities): class SmartTubThermostat(SmartTubEntity, ClimateEntity): """The target water temperature for the spa.""" - PRESET_MODES = { - Spa.HeatMode.AUTO: PRESET_NONE, - Spa.HeatMode.ECONOMY: PRESET_ECO, - Spa.HeatMode.DAY: PRESET_DAY, - } - - HEAT_MODES = {v: k for k, v in PRESET_MODES.items()} - - HVAC_ACTIONS = { - "OFF": CURRENT_HVAC_IDLE, - "ON": CURRENT_HVAC_HEAT, - } - def __init__(self, coordinator, spa): """Initialize the entity.""" - super().__init__(coordinator, spa, "thermostat") + super().__init__(coordinator, spa, "Thermostat") @property def temperature_unit(self): @@ -64,7 +64,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): @property def hvac_action(self): """Return the current running hvac operation.""" - return self.HVAC_ACTIONS.get(self.spa_status.heater) + return HVAC_ACTIONS.get(self.spa_status.heater) @property def hvac_modes(self): @@ -112,12 +112,12 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): @property def preset_mode(self): """Return the current preset mode.""" - return self.PRESET_MODES[self.spa_status.heat_mode] + return PRESET_MODES[self.spa_status.heat_mode] @property def preset_modes(self): """Return the available preset modes.""" - return list(self.PRESET_MODES.values()) + return list(PRESET_MODES.values()) @property def current_temperature(self): @@ -137,6 +137,6 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str): """Activate the specified preset mode.""" - heat_mode = self.HEAT_MODES[preset_mode] + heat_mode = HEAT_MODES[preset_mode] await self.spa.set_heat_mode(heat_mode) await self.coordinator.async_refresh() diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index 8f3ed17f93a..d3349060a07 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN from .controller import SmartTubController DATA_SCHEMA = vol.Schema( diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index 0b97926cc43..23bd8bd8ec0 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -20,3 +20,8 @@ DEFAULT_MAX_TEMP = 40 DEFAULT_LIGHT_EFFECT = "purple" # default to 50% brightness DEFAULT_LIGHT_BRIGHTNESS = 128 + +ATTR_LIGHTS = "lights" +ATTR_PUMPS = "pumps" +ATTR_REMINDERS = "reminders" +ATTR_STATUS = "status" diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index bf8de2f4e2e..8139c72ab6e 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -15,7 +15,15 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, POLLING_TIMEOUT, SCAN_INTERVAL +from .const import ( + ATTR_LIGHTS, + ATTR_PUMPS, + ATTR_REMINDERS, + ATTR_STATUS, + DOMAIN, + POLLING_TIMEOUT, + SCAN_INTERVAL, +) from .helpers import get_spa_name _LOGGER = logging.getLogger(__name__) @@ -86,15 +94,17 @@ class SmartTubController: return data async def _get_spa_data(self, spa): - status, pumps, lights = await asyncio.gather( + status, pumps, lights, reminders = await asyncio.gather( spa.get_status(), spa.get_pumps(), spa.get_lights(), + spa.get_reminders(), ) return { - "status": status, - "pumps": {pump.id: pump for pump in pumps}, - "lights": {light.zone: light for light in lights}, + ATTR_STATUS: status, + ATTR_PUMPS: {pump.id: pump for pump in pumps}, + ATTR_LIGHTS: {light.zone: light for light in lights}, + ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders}, } async def async_register_devices(self, entry): diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index a4ada7c3024..57acf583415 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -13,6 +13,7 @@ from homeassistant.components.light import ( ) from .const import ( + ATTR_LIGHTS, DEFAULT_LIGHT_BRIGHTNESS, DEFAULT_LIGHT_EFFECT, DOMAIN, @@ -32,7 +33,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [ SmartTubLight(controller.coordinator, light) for spa in controller.spas - for light in await spa.get_lights() + for light in controller.coordinator.data[spa.id][ATTR_LIGHTS].values() ] async_add_entities(entities) @@ -137,5 +138,5 @@ class SmartTubLight(SmartTubEntity, LightEntity): async def async_turn_off(self, **kwargs): """Turn the light off.""" - await self.light.set_mode(self.light.LightMode.OFF, 0) + await self.light.set_mode(SpaLight.LightMode.OFF, 0) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 292ce81b4fb..2425268e05c 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -6,7 +6,7 @@ "dependencies": [], "codeowners": ["@mdz"], "requirements": [ - "python-smarttub==0.0.17" + "python-smarttub==0.0.19" ], "quality_scale": "platinum" } diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index be3d60c0241..ea803c9862b 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -2,14 +2,18 @@ from enum import Enum import logging +from homeassistant.components.sensor import SensorEntity + from .const import DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubSensorBase _LOGGER = logging.getLogger(__name__) +# the desired duration, in hours, of the cycle ATTR_DURATION = "duration" -ATTR_LAST_UPDATED = "last_updated" +ATTR_CYCLE_LAST_UPDATED = "cycle_last_updated" ATTR_MODE = "mode" +# the hour of the day at which to start the cycle (0-23) ATTR_START_HOUR = "start_hour" @@ -42,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class SmartTubSensor(SmartTubSensorBase): +class SmartTubSensor(SmartTubSensorBase, SensorEntity): """Generic class for SmartTub status sensors.""" @property @@ -59,7 +63,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): def __init__(self, coordinator, spa): """Initialize the entity.""" super().__init__( - coordinator, spa, "primary filtration cycle", "primary_filtration" + coordinator, spa, "Primary Filtration Cycle", "primary_filtration" ) @property @@ -68,12 +72,12 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): return self._state.status.name.lower() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state = self._state return { ATTR_DURATION: state.duration, - ATTR_LAST_UPDATED: state.last_updated.isoformat(), + ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(), ATTR_MODE: state.mode.name.lower(), ATTR_START_HOUR: state.start_hour, } @@ -94,10 +98,10 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): return self._state.status.name.lower() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state = self._state return { - ATTR_LAST_UPDATED: state.last_updated.isoformat(), + ATTR_CYCLE_LAST_UPDATED: state.last_updated.isoformat(), ATTR_MODE: state.mode.name.lower(), } diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index 7e4c83f6feb..26239df9dff 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -6,7 +6,7 @@ from smarttub import SpaPump from homeassistant.components.switch import SwitchEntity -from .const import API_TIMEOUT, DOMAIN, SMARTTUB_CONTROLLER +from .const import API_TIMEOUT, ATTR_PUMPS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity from .helpers import get_spa_name @@ -21,7 +21,7 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [ SmartTubPump(controller.coordinator, pump) for spa in controller.spas - for pump in await spa.get_pumps() + for pump in controller.coordinator.data[spa.id][ATTR_PUMPS].values() ] async_add_entities(entities) diff --git a/homeassistant/components/smarttub/translations/bg.json b/homeassistant/components/smarttub/translations/bg.json new file mode 100644 index 00000000000..05ef3ed780e --- /dev/null +++ b/homeassistant/components/smarttub/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/de.json b/homeassistant/components/smarttub/translations/de.json index fbb3411a6c5..8e608193b81 100644 --- a/homeassistant/components/smarttub/translations/de.json +++ b/homeassistant/components/smarttub/translations/de.json @@ -13,7 +13,9 @@ "data": { "email": "E-Mail", "password": "Passwort" - } + }, + "description": "Gib deine SmartTub E-Mail-Adresse und Passwort f\u00fcr die Anmeldung ein", + "title": "Anmeldung" } } } diff --git a/homeassistant/components/smarttub/translations/hu.json b/homeassistant/components/smarttub/translations/hu.json new file mode 100644 index 00000000000..666ff85e321 --- /dev/null +++ b/homeassistant/components/smarttub/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + }, + "description": "Add meg SmartTub e-mail c\u00edmet \u00e9s jelsz\u00f3t a bejelentkez\u00e9shez", + "title": "Bejelentkez\u00e9s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/id.json b/homeassistant/components/smarttub/translations/id.json new file mode 100644 index 00000000000..c1de3aa0453 --- /dev/null +++ b/homeassistant/components/smarttub/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + }, + "description": "Masukkan alamat email dan kata sandi SmartTub Anda untuk masuk", + "title": "Masuk" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ko.json b/homeassistant/components/smarttub/translations/ko.json index fab7e511034..2ab844cd967 100644 --- a/homeassistant/components/smarttub/translations/ko.json +++ b/homeassistant/components/smarttub/translations/ko.json @@ -13,7 +13,9 @@ "data": { "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" - } + }, + "description": "\ub85c\uadf8\uc778\ud558\ub824\uba74 SmartTub \uc774\uba54\uc77c \uc8fc\uc18c\uc640 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "\ub85c\uadf8\uc778" } } } diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json index a5f20db8a32..7ef935d8cee 100644 --- a/homeassistant/components/smarttub/translations/nl.json +++ b/homeassistant/components/smarttub/translations/nl.json @@ -14,6 +14,7 @@ "email": "E-mail", "password": "Wachtwoord" }, + "description": "Voer uw SmartTub-e-mailadres en wachtwoord in om in te loggen", "title": "Inloggen" } } diff --git a/homeassistant/components/smarttub/translations/pt.json b/homeassistant/components/smarttub/translations/pt.json new file mode 100644 index 00000000000..414ca7ddf82 --- /dev/null +++ b/homeassistant/components/smarttub/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index 22987673005..72d5071882f 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -59,12 +59,12 @@ def setup(hass, config): def poll_device_update(event_time): """Update Smarty device.""" - _LOGGER.debug("Updating Smarty device...") + _LOGGER.debug("Updating Smarty device") if smarty.update(): - _LOGGER.debug("Update success...") + _LOGGER.debug("Update success") dispatcher_send(hass, SIGNAL_UPDATE_SMARTY) else: - _LOGGER.debug("Update failed...") + _LOGGER.debug("Update failed") track_time_interval(hass, poll_device_update, timedelta(seconds=30)) diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index f5cd1fbb404..b958185f9bd 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -3,6 +3,7 @@ import datetime as dt import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, @@ -10,7 +11,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util from . import DOMAIN, SIGNAL_UPDATE_SMARTY @@ -35,7 +35,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, True) -class SmartySensor(Entity): +class SmartySensor(SensorEntity): """Representation of a Smarty Sensor.""" def __init__( diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 8853680af33..a8cfdba5be5 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -53,31 +53,32 @@ class SmhiFlowHandler(config_entries.ConfigFlow): # If hass config has the location set and is a valid coordinate the # default location is set as default values in the form - if not smhi_locations(self.hass): - if await self._homeassistant_location_exists(): - return await self._show_config_form( - name=HOME_LOCATION_NAME, - latitude=self.hass.config.latitude, - longitude=self.hass.config.longitude, - ) + if ( + not smhi_locations(self.hass) + and await self._homeassistant_location_exists() + ): + return await self._show_config_form( + name=HOME_LOCATION_NAME, + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude, + ) return await self._show_config_form() async def _homeassistant_location_exists(self) -> bool: """Return true if default location is set and is valid.""" - if self.hass.config.latitude != 0.0 and self.hass.config.longitude != 0.0: - # Return true if valid location - if await self._check_location( + # Return true if valid location + return ( + self.hass.config.latitude != 0.0 + and self.hass.config.longitude != 0.0 + and await self._check_location( self.hass.config.longitude, self.hass.config.latitude - ): - return True - return False + ) + ) def _name_in_configuration_exists(self, name: str) -> bool: """Return True if name exists in configuration.""" - if name in smhi_locations(self.hass): - return True - return False + return name in smhi_locations(self.hass) async def _show_config_form( self, name: str = None, latitude: str = None, longitude: str = None @@ -97,7 +98,6 @@ class SmhiFlowHandler(config_entries.ConfigFlow): async def _check_location(self, longitude: str, latitude: str) -> bool: """Return true if location is ok.""" - try: session = aiohttp_client.async_get_clientsession(self.hass) smhi_api = Smhi(longitude, latitude, session=session) diff --git a/homeassistant/components/smhi/translations/he.json b/homeassistant/components/smhi/translations/he.json new file mode 100644 index 00000000000..4c49313d977 --- /dev/null +++ b/homeassistant/components/smhi/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/id.json b/homeassistant/components/smhi/translations/id.json new file mode 100644 index 00000000000..8d5d95f183e --- /dev/null +++ b/homeassistant/components/smhi/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Nama sudah ada", + "wrong_location": "Hanya untuk lokasi di Swedia" + }, + "step": { + "user": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" + }, + "title": "Lokasi di Swedia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index c13982ee15d..86cdf72e65c 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -1,8 +1,9 @@ """Support for the Swedish weather institute weather service.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Dict, List import aiohttp import async_timeout @@ -210,7 +211,7 @@ class SmhiWeather(WeatherEntity): return "Swedish weather institute (SMHI)" @property - def forecast(self) -> List: + def forecast(self) -> list: """Return the forecast.""" if self._forecasts is None or len(self._forecasts) < 2: return None @@ -235,7 +236,7 @@ class SmhiWeather(WeatherEntity): return data @property - def device_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Return SMHI specific attributes.""" if self.cloudiness: return {ATTR_SMHI_CLOUDINESS: self.cloudiness} diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 8752dfd90da..c4fdb38ebaa 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -46,9 +46,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not gateway: return False hass.data[DOMAIN][SMS_GATEWAY] = gateway - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -59,8 +59,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index 01c1d182c93..9546fba0773 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -1,13 +1,13 @@ """Config flow for SMS integration.""" import logging -import gammu # pylint: disable=import-error, no-member +import gammu # pylint: disable=import-error import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_DEVICE -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN from .gateway import create_sms_gateway _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 9bdfbad0f55..51667ef8f77 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -1,10 +1,8 @@ """The sms gateway to interact with a GSM modem.""" import logging -import gammu # pylint: disable=import-error, no-member -from gammu.asyncworker import ( # pylint: disable=import-error, no-member - GammuAsyncWorker, -) +import gammu # pylint: disable=import-error +from gammu.asyncworker import GammuAsyncWorker # pylint: disable=import-error from homeassistant.core import callback @@ -45,7 +43,7 @@ class Gateway: ) entries = self.get_and_delete_all_sms(state_machine) _LOGGER.debug("SMS entries:%s", entries) - data = list() + data = [] for entry in entries: decoded_entry = gammu.DecodeSMS(entry) @@ -80,7 +78,7 @@ class Gateway: start_remaining = remaining # Get all sms start = True - entries = list() + entries = [] all_parts = -1 all_parts_arrived = False _LOGGER.debug("Start remaining:%i", start_remaining) @@ -165,6 +163,6 @@ async def create_sms_gateway(config, hass): gateway = Gateway(worker, hass) await gateway.init_async() return gateway - except gammu.GSMError as exc: # pylint: disable=no-member + except gammu.GSMError as exc: _LOGGER.error("Failed to initialize, error %s", exc) return None diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index f030409b6ca..04964c15878 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -1,7 +1,7 @@ """Support for SMS notification services.""" import logging -import gammu # pylint: disable=import-error, no-member +import gammu # pylint: disable=import-error import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService @@ -51,8 +51,8 @@ class SMSNotificationService(BaseNotificationService): } try: # Encode messages - encoded = gammu.EncodeSMS(smsinfo) # pylint: disable=no-member - except gammu.GSMError as exc: # pylint: disable=no-member + encoded = gammu.EncodeSMS(smsinfo) + except gammu.GSMError as exc: _LOGGER.error("Encoding message %s failed: %s", message, exc) return @@ -64,5 +64,5 @@ class SMSNotificationService(BaseNotificationService): try: # Actually send the message await self.gateway.send_sms_async(encoded_message) - except gammu.GSMError as exc: # pylint: disable=no-member + except gammu.GSMError as exc: _LOGGER.error("Sending to %s failed: %s", self.number, exc) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index eaad395eaa6..fc2310426e3 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -1,10 +1,10 @@ """Support for SMS dongle sensor.""" import logging -import gammu # pylint: disable=import-error, no-member +import gammu # pylint: disable=import-error +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS -from homeassistant.helpers.entity import Entity from .const import DOMAIN, SMS_GATEWAY @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class GSMSignalSensor(Entity): +class GSMSignalSensor(SensorEntity): """Implementation of a GSM Signal sensor.""" def __init__( @@ -71,11 +71,11 @@ class GSMSignalSensor(Entity): """Get the latest data from the modem.""" try: self._state = await self._gateway.get_signal_quality_async() - except gammu.GSMError as exc: # pylint: disable=no-member + except gammu.GSMError as exc: _LOGGER.error("Failed to read signal quality: %s", exc) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the sensor attributes.""" return self._state diff --git a/homeassistant/components/sms/translations/hu.json b/homeassistant/components/sms/translations/hu.json index 3b2d79a34a7..6fa524b18ab 100644 --- a/homeassistant/components/sms/translations/hu.json +++ b/homeassistant/components/sms/translations/hu.json @@ -1,7 +1,20 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "device": "Eszk\u00f6z" + }, + "title": "Csatlakoz\u00e1s a modemhez" + } } } } \ No newline at end of file diff --git a/homeassistant/components/sms/translations/id.json b/homeassistant/components/sms/translations/id.json new file mode 100644 index 00000000000..63ebb088521 --- /dev/null +++ b/homeassistant/components/sms/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "device": "Perangkat" + }, + "title": "Hubungkan ke modem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/ko.json b/homeassistant/components/sms/translations/ko.json index 13c043ef635..5ead95c1a27 100644 --- a/homeassistant/components/sms/translations/ko.json +++ b/homeassistant/components/sms/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 4e65b60280b..43fbbeb8808 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -2,6 +2,6 @@ "domain": "snapcast", "name": "Snapcast", "documentation": "https://www.home-assistant.io/integrations/snapcast", - "requirements": ["snapcast==2.1.1"], + "requirements": ["snapcast==2.1.2"], "codeowners": [] } diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index ab4b2415034..e1c5b7d875b 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -164,7 +164,7 @@ class SnapcastGroupDevice(MediaPlayerEntity): return list(self._group.streams_by_name().keys()) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" name = f"{self._group.friendly_name} {GROUP_SUFFIX}" return {"friendly_name": name} @@ -261,7 +261,7 @@ class SnapcastClientDevice(MediaPlayerEntity): return STATE_OFF @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" state_attrs = {} if self.latency is not None: diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index a60183a1a0f..7de2bfb91e2 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -15,7 +15,7 @@ from pysnmp.hlapi.asyncio import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -26,7 +26,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from .const import ( CONF_ACCEPT_ERRORS, @@ -139,7 +138,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([SnmpSensor(data, name, unit, value_template)], True) -class SnmpSensor(Entity): +class SnmpSensor(SensorEntity): """Representation of a SNMP sensor.""" def __init__(self, data, name, unit_of_measurement, value_template): diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index 8f704471339..1f735da4995 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -4,11 +4,10 @@ from datetime import timedelta from pysochain import ChainSo import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTRIBUTION = "Data provided by chain.so" @@ -40,7 +39,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([SochainSensor(name, network.upper(), chainso)], True) -class SochainSensor(Entity): +class SochainSensor(SensorEntity): """Representation of a Sochain sensor.""" def __init__(self, name, unit_of_measurement, chainso): @@ -69,7 +68,7 @@ class SochainSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py index 3d53e76a27a..e38c45d10b4 100644 --- a/homeassistant/components/socialblade/sensor.py +++ b/homeassistant/components/socialblade/sensor.py @@ -5,10 +5,9 @@ import logging import socialbladeclient import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -42,7 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([social_blade]) -class SocialBladeSensor(Entity): +class SocialBladeSensor(SensorEntity): """Representation of a Social Blade Sensor.""" def __init__(self, case, name): @@ -64,7 +63,7 @@ class SocialBladeSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._attributes: return self._attributes diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index bafc6b67f1c..e054abfe8ae 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -9,15 +9,18 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SITE_ID): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SITE_ID): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index f0a620021ad..5cfe773d98c 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge", "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"], "config_flow": true, - "codeowners": [], - "dhcp": [{"hostname":"target","macaddress":"002702*"}] + "codeowners": ["@frenck"], + "dhcp": [{ "hostname": "target", "macaddress": "002702*" }] } diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 8609e578e5e..7835fa9aee4 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -7,10 +7,10 @@ from requests.exceptions import ConnectTimeout, HTTPError import solaredge from stringcase import snakecase +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -117,7 +117,7 @@ class SolarEdgeSensorFactory: return sensor_class(self.platform_name, sensor_key, service) -class SolarEdgeSensor(CoordinatorEntity, Entity): +class SolarEdgeSensor(CoordinatorEntity, SensorEntity): """Abstract class for a solaredge sensor.""" def __init__(self, platform_name, sensor_key, data_service): @@ -162,7 +162,7 @@ class SolarEdgeDetailsSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API details sensor.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self.data_service.attributes @@ -182,7 +182,7 @@ class SolarEdgeInventorySensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self.data_service.attributes.get(self._json_key) @@ -202,7 +202,7 @@ class SolarEdgeEnergyDetailsSensor(SolarEdgeSensor): self._json_key = SENSOR_TYPES[self.sensor_key][0] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self.data_service.attributes.get(self._json_key) @@ -232,7 +232,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensor): return DEVICE_CLASS_POWER @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self.data_service.attributes.get(self._json_key) diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index 31890269925..8479c90f595 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", "site_not_active": "Az oldal nem akt\u00edv" }, "step": { diff --git a/homeassistant/components/solaredge/translations/id.json b/homeassistant/components/solaredge/translations/id.json new file mode 100644 index 00000000000..41c94755af7 --- /dev/null +++ b/homeassistant/components/solaredge/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "site_exists": "Nilai site_id ini sudah dikonfigurasi" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "could_not_connect": "Tidak dapat terhubung ke API solaredge", + "invalid_api_key": "Kunci API tidak valid", + "site_exists": "Nilai site_id ini sudah dikonfigurasi", + "site_not_active": "Situs tidak aktif" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama instalasi ini", + "site_id": "Nilai site_id SolarEdge" + }, + "title": "Tentukan parameter API untuk instalasi ini" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/ko.json b/homeassistant/components/solaredge/translations/ko.json index 8544cdd143d..ce3ed2a767d 100644 --- a/homeassistant/components/solaredge/translations/ko.json +++ b/homeassistant/components/solaredge/translations/ko.json @@ -6,8 +6,10 @@ }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "could_not_connect": "SolarEdge API\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "site_not_active": "\uc0ac\uc774\ud2b8\uac00 \ud65c\uc131\ud654\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/nl.json b/homeassistant/components/solaredge/translations/nl.json index 3fe28971f29..24e1716dd57 100644 --- a/homeassistant/components/solaredge/translations/nl.json +++ b/homeassistant/components/solaredge/translations/nl.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "api_key": "De API-sleutel voor deze site", + "api_key": "API-sleutel", "name": "De naam van deze installatie", "site_id": "De SolarEdge site-id" }, diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 59b0a5e8856..441a1c39e08 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,4 +1,5 @@ """Support for SolarEdge-local Monitoring API.""" +from contextlib import suppress from copy import deepcopy from datetime import timedelta import logging @@ -8,7 +9,7 @@ from requests.exceptions import ConnectTimeout, HTTPError from solaredge_local import SolarEdge import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, @@ -21,7 +22,6 @@ from homeassistant.const import ( VOLT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle DOMAIN = "solaredge_local" @@ -231,7 +231,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class SolarEdgeSensor(Entity): +class SolarEdgeSensor(SensorEntity): """Representation of an SolarEdge Monitoring API sensor.""" def __init__(self, platform_name, data, json_key, name, unit, icon, attr): @@ -257,7 +257,7 @@ class SolarEdgeSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._attr: try: @@ -351,19 +351,15 @@ class SolarEdgeData: self.info["optimizers"] = status.optimizersStatus.total self.info["invertertemperature"] = INVERTER_MODES[status.status] - try: + with suppress(IndexError): if status.metersList[1]: self.data["currentPowerimport"] = status.metersList[1].currentPower self.data["totalEnergyimport"] = status.metersList[1].totalEnergy - except IndexError: - pass - try: + with suppress(IndexError): if status.metersList[0]: self.data["currentPowerexport"] = status.metersList[0].currentPower self.data["totalEnergyexport"] = status.metersList[0].totalEnergy - except IndexError: - pass if maintenance.system.name: self.data["optimizertemperature"] = round(statistics.mean(temperature), 2) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 6073d12815b..85a1531090d 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -5,8 +5,8 @@ from urllib.parse import ParseResult, urlparse from requests.exceptions import HTTPError, Timeout from sunwatcher.solarlog.solarlog import SolarLog +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_HOST -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from .const import DOMAIN, SCAN_INTERVAL, SENSOR_TYPES @@ -55,7 +55,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return True -class SolarlogSensor(Entity): +class SolarlogSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, entry_id, device_name, sensor_key, data): diff --git a/homeassistant/components/solarlog/translations/hu.json b/homeassistant/components/solarlog/translations/hu.json index 3fa8a9620a0..dd0ea8033ae 100644 --- a/homeassistant/components/solarlog/translations/hu.json +++ b/homeassistant/components/solarlog/translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { diff --git a/homeassistant/components/solarlog/translations/id.json b/homeassistant/components/solarlog/translations/id.json new file mode 100644 index 00000000000..3ce222f9c2a --- /dev/null +++ b/homeassistant/components/solarlog/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Prefiks yang akan digunakan untuk sensor Solar-Log Anda" + }, + "title": "Tentukan koneksi Solar-Log Anda" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/nl.json b/homeassistant/components/solarlog/translations/nl.json index 8ccf5e626c7..6e526802d8a 100644 --- a/homeassistant/components/solarlog/translations/nl.json +++ b/homeassistant/components/solarlog/translations/nl.json @@ -5,12 +5,12 @@ }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Verbinding mislukt, controleer het host-adres" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { "data": { - "host": "De hostnaam of het IP-adres van uw Solar-Log apparaat", + "host": "Host", "name": "Het voorvoegsel dat moet worden gebruikt voor uw Solar-Log sensoren" }, "title": "Definieer uw Solar-Log verbinding" diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 232715ebe18..90bfd8e6184 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -2,6 +2,6 @@ "domain": "solax", "name": "SolaX Power", "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": ["solax==0.2.5"], + "requirements": ["solax==0.2.6"], "codeowners": ["@squishykid"] } diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index bca507c4391..e47f5c57802 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -6,11 +6,10 @@ from solax import real_time_api from solax.inverter import InverterError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval DEFAULT_PORT = 80 @@ -73,7 +72,7 @@ class RealTimeDataEndpoint: sensor.async_schedule_update_ha_state() -class Inverter(Entity): +class Inverter(SensorEntity): """Class for a sensor.""" def __init__(self, uid, serial, key, unit): diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index bd5695cb7ec..3f15199c162 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -24,7 +24,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMA_COMPONENTS = ["cover", "sensor"] +PLATFORMS = ["cover", "sensor"] async def async_setup(hass, config): @@ -50,9 +50,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) hass.data[DOMAIN][DEVICES] = devices["shades"] - for component in SOMA_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -63,8 +63,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SOMA_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 9430a929e1e..436a92a1087 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -4,8 +4,8 @@ import logging from requests import RequestException +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from . import DEVICES, SomaEntity @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class SomaSensor(SomaEntity, Entity): +class SomaSensor(SomaEntity, SensorEntity): """Representation of a Soma cover device.""" @property diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index 82ec28ff4d7..d013cb49fdf 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.", "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." @@ -15,7 +16,7 @@ "port": "Port" }, "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", - "title": "SOMA csatlakoz\u00e1s" + "title": "SOMA Connect" } } } diff --git a/homeassistant/components/soma/translations/id.json b/homeassistant/components/soma/translations/id.json new file mode 100644 index 00000000000..d512bd46797 --- /dev/null +++ b/homeassistant/components/soma/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "Anda hanya dapat mengonfigurasi satu akun Soma.", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "connection_error": "Gagal menyambungkan ke SOMA Connect.", + "missing_configuration": "Komponen Soma tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "result_error": "SOMA Connect merespons dengan status kesalahan." + }, + "create_entry": { + "default": "Berhasil mengautentikasi dengan Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Masukkan pengaturan koneksi SOMA Connect Anda.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/ko.json b/homeassistant/components/soma/translations/ko.json index 83c2f01ff8b..13a6fb03e83 100644 --- a/homeassistant/components/soma/translations/ko.json +++ b/homeassistant/components/soma/translations/ko.json @@ -3,12 +3,12 @@ "abort": { "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "connection_error": "SOMA Connect \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "connection_error": "SOMA Connect\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "result_error": "SOMA Connect \uac00 \uc624\ub958 \uc0c1\ud0dc\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4." }, "create_entry": { - "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "Soma\ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { "user": { diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index ac32b9d5379..9d67675f10e 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -48,7 +48,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["climate", "cover", "sensor", "switch"] +PLATFORMS = ["climate", "cover", "sensor", "switch"] async def async_setup(hass, config): @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): ) data[COORDINATOR] = coordinator - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() if all(not bool(device.states) for device in coordinator.data.values()): _LOGGER.debug( @@ -134,9 +134,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): model=hub.type, ) - for component in SOMFY_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -147,8 +147,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data[DOMAIN].pop(API, None) await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SOMFY_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) return True diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index a679af06d73..43db2c29060 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -1,6 +1,7 @@ """API for Somfy bound to Home Assistant OAuth.""" +from __future__ import annotations + from asyncio import run_coroutine_threadsafe -from typing import Dict, Union from pymfy.api import somfy_api @@ -27,7 +28,7 @@ class ConfigEntrySomfyApi(somfy_api.SomfyApi): def refresh_tokens( self, - ) -> Dict[str, Union[str, int]]: + ) -> dict[str, str | int]: """Refresh and return new Somfy tokens using Home Assistant OAuth2 session.""" run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self.hass.loop diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index 99d6dca06ee..66602aea3e6 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -1,6 +1,5 @@ """Support for Somfy Thermostat.""" - -from typing import List, Optional +from __future__ import annotations from pymfy.api.devices.category import Category from pymfy.api.devices.thermostat import ( @@ -125,7 +124,7 @@ class SomfyClimate(SomfyEntity, ClimateEntity): return HVAC_MODES_MAPPING.get(self._climate.get_hvac_state()) @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. HEAT and COOL mode are exclusive. End user has to enable a mode manually within the Somfy application. @@ -144,13 +143,13 @@ class SomfyClimate(SomfyEntity, ClimateEntity): ) @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode.""" mode = self._climate.get_target_mode() return PRESETS_MAPPING.get(mode) @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return list(PRESETS_MAPPING.values()) diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py index 996a95348a4..34283a1271c 100644 --- a/homeassistant/components/somfy/sensor.py +++ b/homeassistant/components/somfy/sensor.py @@ -3,6 +3,7 @@ from pymfy.api.devices.category import Category from pymfy.api.devices.thermostat import Thermostat +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from . import SomfyEntity @@ -26,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors) -class SomfyThermostatBatterySensor(SomfyEntity): +class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): """Representation of a Somfy thermostat battery.""" def __init__(self, coordinator, device_id, api): diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json index 86927570c85..ce4e94b3399 100644 --- a/homeassistant/components/somfy/translations/hu.json +++ b/homeassistant/components/somfy/translations/hu.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "create_entry": { - "default": "Sikeres autentik\u00e1ci\u00f3" + "default": "Sikeres hiteles\u00edt\u00e9s" }, "step": { "pick_implementation": { - "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/somfy/translations/id.json b/homeassistant/components/somfy/translations/id.json new file mode 100644 index 00000000000..2d229de00d5 --- /dev/null +++ b/homeassistant/components/somfy/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json index 8b4f4ff752f..568c8d05116 100644 --- a/homeassistant/components/somfy/translations/ko.json +++ b/homeassistant/components/somfy/translations/ko.json @@ -4,7 +4,7 @@ "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.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json index b7f077f2c73..94305c7ae6f 100644 --- a/homeassistant/components/somfy/translations/nl.json +++ b/homeassistant/components/somfy/translations/nl.json @@ -2,16 +2,16 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "Het Somfy-component is niet geconfigureerd. Gelieve de documentatie te volgen.", + "missing_configuration": "De Netatmo-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." }, "create_entry": { - "default": "Succesvol geverifieerd met Somfy." + "default": "Succesvol geauthenticeerd" }, "step": { "pick_implementation": { - "title": "Kies de authenticatie methode" + "title": "Kies een authenticatie methode" } } } diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index d371fd96310..40240306dc4 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -23,7 +23,7 @@ from .const import ( DEFAULT_PORT, DOMAIN, MYLINK_STATUS, - SOMFY_MYLINK_COMPONENTS, + PLATFORMS, ) CONFIG_OPTIONS = (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG) @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not mylink_status or "error" in mylink_status: _LOGGER.error( - "mylink failed to setup because of an error: %s", + "Somfy Mylink failed to setup because of an error: %s", mylink_status.get("error", {}).get( "message", "Empty response from mylink device" ), @@ -121,9 +121,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UNDO_UPDATE_LISTENER: undo_listener, } - for component in SOMFY_MYLINK_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -182,8 +182,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SOMFY_MYLINK_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index b6d647b9a3b..d1a1e19609a 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -19,9 +19,9 @@ from .const import ( CONF_TARGET_ID, CONF_TARGET_NAME, DEFAULT_PORT, + DOMAIN, MYLINK_STATUS, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py index a7cbf864cd9..bf58ee1af92 100644 --- a/homeassistant/components/somfy_mylink/const.py +++ b/homeassistant/components/somfy_mylink/const.py @@ -14,6 +14,6 @@ DATA_SOMFY_MYLINK = "somfy_mylink_data" MYLINK_STATUS = "mylink_status" DOMAIN = "somfy_mylink" -SOMFY_MYLINK_COMPONENTS = ["cover"] +PLATFORMS = ["cover"] MANUFACTURER = "Somfy" diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index a7be33583d2..a71661f57f4 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "somfy-mylink-synergy==1.0.6" ], - "codeowners": ["@bdraco"], + "codeowners": [], "config_flow": true, "dhcp": [{ "hostname":"somfy_*", "macaddress":"B8B7F1*" diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json index 522e185af5d..4382ccd4a0c 100644 --- a/homeassistant/components/somfy_mylink/translations/de.json +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -25,9 +25,13 @@ }, "step": { "entity_config": { + "description": "Optionen f\u00fcr `{entity_id}` konfigurieren", "title": "Entit\u00e4t konfigurieren" }, "init": { + "data": { + "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t." + }, "title": "MyLink-Optionen konfigurieren" }, "target_config": { @@ -35,5 +39,6 @@ "title": "MyLink-Cover konfigurieren" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/he.json b/homeassistant/components/somfy_mylink/translations/he.json new file mode 100644 index 00000000000..9af5985ac45 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/he.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "init": { + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc MyLink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json new file mode 100644 index 00000000000..08d0db14866 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -0,0 +1,42 @@ +{ + "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": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "entity_config": { + "title": "Entit\u00e1s konfigur\u00e1l\u00e1sa" + }, + "init": { + "data": { + "target_id": "Az \u00e1rny\u00e9kol\u00f3 be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa." + }, + "title": "Mylink be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" + }, + "target_config": { + "description": "A(z) `{target_name}` be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa", + "title": "MyLink \u00e1rny\u00e9kol\u00f3 konfigur\u00e1l\u00e1sa" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/id.json b/homeassistant/components/somfy_mylink/translations/id.json new file mode 100644 index 00000000000..0203ae421e2 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/id.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "ID Sistem" + }, + "description": "ID Sistem dapat diperoleh di aplikasi MyLink di bawah bagian Integrasi dengan memilih layanan non-Cloud." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Penutup dibalik" + }, + "description": "Konfigurasikan opsi untuk `{entity_id}`", + "title": "Konfigurasikan Entitas" + }, + "init": { + "data": { + "default_reverse": "Status pembalikan baku untuk penutup yang belum dikonfigurasi", + "entity_id": "Konfigurasikan entitas tertentu.", + "target_id": "Konfigurasikan opsi untuk penutup." + }, + "title": "Konfigurasikan Opsi MyLink" + }, + "target_config": { + "data": { + "reverse": "Penutup dibalik" + }, + "description": "Konfigurasikan opsi untuk `{target_name}`", + "title": "Konfigurasikan Cover MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ko.json b/homeassistant/components/somfy_mylink/translations/ko.json index 4d4a78ee1f0..b099d3d6ed8 100644 --- a/homeassistant/components/somfy_mylink/translations/ko.json +++ b/homeassistant/components/somfy_mylink/translations/ko.json @@ -8,18 +8,46 @@ "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": "Somfy MyLink: {mac} ({ip})", "step": { "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8" - } + "port": "\ud3ec\ud2b8", + "system_id": "\uc2dc\uc2a4\ud15c ID" + }, + "description": "\uc2dc\uc2a4\ud15c ID\ub294 MyLink \uc571\uc758 Integration\uc5d0\uc11c non-Cloud service\ub97c \uc120\ud0dd\ud558\uc5ec \uc5bb\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4." } } }, "options": { "abort": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "entity_config": { + "data": { + "reverse": "\uc5ec\ub2eb\uc774\uac00 \ubc18\uc804\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "description": "`{entity_id}`\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30", + "title": "\uad6c\uc131\uc694\uc18c \uad6c\uc131\ud558\uae30" + }, + "init": { + "data": { + "default_reverse": "\uad6c\uc131\ub418\uc9c0 \uc54a\uc740 \uc5ec\ub2eb\uc774\uc5d0 \ub300\ud55c \uae30\ubcf8 \ubc18\uc804 \uc0c1\ud0dc", + "entity_id": "\ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "target_id": "\uc5ec\ub2eb\uc774\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30" + }, + "title": "MyLink \uc635\uc158 \uad6c\uc131\ud558\uae30" + }, + "target_config": { + "data": { + "reverse": "\uc5ec\ub2eb\uc774\uac00 \ubc18\uc804\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "description": "`{target_name}`\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30", + "title": "MyLink \uc5ec\ub2eb\uc774 \uad6c\uc131\ud558\uae30" + } } - } + }, + "title": "Somfy MyLink" } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index b0ae5c9d3ad..b900e46bee4 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -15,7 +15,8 @@ "host": "Host", "port": "Poort", "system_id": "Systeem-ID" - } + }, + "description": "De systeem-id kan worden verkregen in de MyLink app onder Integratie door een niet-Cloud service te selecteren." } } }, @@ -25,14 +26,26 @@ }, "step": { "entity_config": { + "data": { + "reverse": "Cover is omgekeerd" + }, "description": "Configureer opties voor `{entity_id}`", "title": "Entiteit configureren" }, "init": { "data": { - "entity_id": "Configureer een specifieke entiteit." + "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde covers", + "entity_id": "Configureer een specifieke entiteit.", + "target_id": "Configureer opties voor een cover." }, "title": "Configureer MyLink-opties" + }, + "target_config": { + "data": { + "reverse": "Cover is omgekeerd" + }, + "description": "Configureer opties voor ' {target_name} '", + "title": "Configureer MyLink Cover" } } }, diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 3e2a5498c55..946d9b1e047 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -1,8 +1,10 @@ """The Sonarr component.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any from sonarr import Sonarr, SonarrAccessRestricted, SonarrError @@ -40,7 +42,7 @@ SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: +async def async_setup(hass: HomeAssistantType, config: dict) -> bool: """Set up the Sonarr component.""" hass.data.setdefault(DOMAIN, {}) return True @@ -84,9 +86,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -97,8 +99,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -164,7 +166,7 @@ class SonarrEntity(Entity): return self._enabled_default @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about the application.""" if self._device_id is None: return None diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index fc11790356a..fd7315585dc 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Sonarr.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from sonarr import Sonarr, SonarrAccessRestricted, SonarrError import voluptuous as vol @@ -27,13 +29,13 @@ from .const import ( DEFAULT_UPCOMING_DAYS, DEFAULT_VERIFY_SSL, DEFAULT_WANTED_MAX_ITEMS, + DOMAIN, ) -from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: +async def validate_input(hass: HomeAssistantType, 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. @@ -73,9 +75,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth( - self, data: Optional[ConfigType] = None - ) -> Dict[str, Any]: + async def async_step_reauth(self, data: ConfigType | None = None) -> dict[str, Any]: """Handle configuration by re-auth.""" self._reauth = True self._entry_data = dict(data) @@ -84,8 +84,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form( @@ -98,8 +98,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_user() async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" errors = {} @@ -138,7 +138,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_reauth_update_entry( self, entry_id: str, data: dict - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Update existing config entry.""" entry = self.hass.config_entries.async_get_entry(entry_id) self.hass.config_entries.async_update_entry(entry, data=data) @@ -146,7 +146,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") - def _get_user_data_schema(self) -> Dict[str, Any]: + def _get_user_data_schema(self) -> dict[str, Any]: """Get the data schema to display user form.""" if self._reauth: return {vol.Required(CONF_API_KEY): str} @@ -174,7 +174,7 @@ class SonarrOptionsFlowHandler(OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: Optional[ConfigType] = None): + async def async_step_init(self, user_input: ConfigType | None = None): """Manage Sonarr options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 8a625846744..3446130433e 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,10 +1,13 @@ """Support for Sonarr sensors.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable from sonarr import Sonarr, SonarrConnectionError, SonarrError +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_GIGABYTES from homeassistant.helpers.entity import Entity @@ -20,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Sonarr sensors based on a config entry.""" options = entry.options @@ -63,7 +66,7 @@ def sonarr_exception_handler(func): return handler -class SonarrSensor(SonarrEntity): +class SonarrSensor(SonarrEntity, SensorEntity): """Implementation of the Sonarr sensor.""" def __init__( @@ -75,7 +78,7 @@ class SonarrSensor(SonarrEntity): icon: str, key: str, name: str, - unit_of_measurement: Optional[str] = None, + unit_of_measurement: str | None = None, ) -> None: """Initialize Sonarr sensor.""" self._unit_of_measurement = unit_of_measurement @@ -131,7 +134,7 @@ class SonarrCommandsSensor(SonarrSensor): self._commands = await self.sonarr.commands() @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" attrs = {} @@ -172,7 +175,7 @@ class SonarrDiskspaceSensor(SonarrSensor): self._total_free = sum([disk.free for disk in self._disks]) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" attrs = {} @@ -217,7 +220,7 @@ class SonarrQueueSensor(SonarrSensor): self._queue = await self.sonarr.queue() @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" attrs = {} @@ -258,7 +261,7 @@ class SonarrSeriesSensor(SonarrSensor): self._items = await self.sonarr.series() @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" attrs = {} @@ -301,7 +304,7 @@ class SonarrUpcomingSensor(SonarrSensor): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" attrs = {} @@ -323,7 +326,7 @@ class SonarrWantedSensor(SonarrSensor): """Initialize Sonarr Wanted sensor.""" self._max_items = max_items self._results = None - self._total: Optional[int] = None + self._total: int | None = None super().__init__( sonarr=sonarr, @@ -342,7 +345,7 @@ class SonarrWantedSensor(SonarrSensor): self._total = self._results.total @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" attrs = {} @@ -354,6 +357,6 @@ class SonarrWantedSensor(SonarrSensor): return attrs @property - def state(self) -> Optional[int]: + def state(self) -> int | None: """Return the state of the sensor.""" return self._total diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index 19a37dbcc4f..939c5eed1c8 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -12,6 +12,7 @@ "flow_title": "Sonarr: {name}", "step": { "reauth_confirm": { + "description": "Die Sonarr-Integration muss manuell mit der Sonarr-API, die unter {host} gehostet wird, neu authentifiziert werden", "title": "Integration erneut authentifizieren" }, "user": { diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index f5301e874ea..e3fa0b5ff21 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -1,7 +1,39 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "flow_title": "Sonarr: {name}", + "step": { + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "base_path": "El\u00e9r\u00e9si \u00fat az API-hoz", + "host": "Hoszt", + "port": "Port", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "A megjelen\u00edteni k\u00edv\u00e1nt k\u00f6vetkez\u0151 napok sz\u00e1ma", + "wanted_max_items": "A megjelen\u00edteni k\u00edv\u00e1nt elemek maxim\u00e1lis sz\u00e1ma" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/id.json b/homeassistant/components/sonarr/translations/id.json new file mode 100644 index 00000000000..ffaf1d22604 --- /dev/null +++ b/homeassistant/components/sonarr/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "Sonarr: {name}", + "step": { + "reauth_confirm": { + "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {host}", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "base_path": "Jalur ke API", + "host": "Host", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Jumlah hari mendatang untuk ditampilkan", + "wanted_max_items": "Jumlah maksimal item yang diinginkan untuk ditampilkan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/ko.json b/homeassistant/components/sonarr/translations/ko.json index 17e3592d509..fbff72e46f2 100644 --- a/homeassistant/components/sonarr/translations/ko.json +++ b/homeassistant/components/sonarr/translations/ko.json @@ -12,7 +12,8 @@ "flow_title": "Sonarr: {name}", "step": { "reauth_confirm": { - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + "description": "Sonarr \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 {host}\uc5d0\uc11c \ud638\uc2a4\ud305\ub418\ub294 Sonarr API\ub85c \uc218\ub3d9\uc73c\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index 08ef9bb2ece..3203f3ac128 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -9,6 +9,7 @@ "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie" }, + "flow_title": "Sonarr: {name}", "step": { "reauth_confirm": { "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {host}", @@ -17,6 +18,7 @@ "user": { "data": { "api_key": "API-sleutel", + "base_path": "Pad naar API", "host": "Host", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", @@ -24,5 +26,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Aantal komende dagen om weer te geven", + "wanted_max_items": "Maximaal aantal gewenste items dat moet worden weergegeven" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index aaa9302cac2..22f9f0932ed 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure songpal component.""" +from __future__ import annotations + import logging -from typing import Optional from urllib.parse import urlparse from songpal import Device, SongpalException @@ -11,7 +12,7 @@ from homeassistant.components import ssdp from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import +from .const import CONF_ENDPOINT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -34,7 +35,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the flow.""" - self.conf: Optional[SongpalConfig] = None + self.conf: SongpalConfig | None = None async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json index cd4c501ecf7..aa55862c0aa 100644 --- a/homeassistant/components/songpal/translations/hu.json +++ b/homeassistant/components/songpal/translations/hu.json @@ -1,12 +1,17 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_songpal_device": "Nem Songpal eszk\u00f6z" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "Sony Songpal {name} ({host})", "step": { + "init": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?" + }, "user": { "data": { "endpoint": "V\u00e9gpont" diff --git a/homeassistant/components/songpal/translations/id.json b/homeassistant/components/songpal/translations/id.json new file mode 100644 index 00000000000..2b8149661bc --- /dev/null +++ b/homeassistant/components/songpal/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "not_songpal_device": "Bukan perangkat Songpal" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "Ingin menyiapkan {name} ({host})?" + }, + "user": { + "data": { + "endpoint": "Titik Akhir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 6b6c927ca1c..179fc62e0cc 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -1,4 +1,5 @@ """Support for media browsing.""" +from contextlib import suppress import logging import urllib.parse @@ -75,10 +76,8 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): children = [] for item in media: - try: + with suppress(UnknownMediaType): children.append(item_payload(item, get_thumbnail_url)) - except UnknownMediaType: - pass return BrowseMedia( title=title, @@ -136,10 +135,8 @@ def library_payload(media_library, get_thumbnail_url=None): children = [] for item in media_library.browse(): - try: + with suppress(UnknownMediaType): children.append(item_payload(item, get_thumbnail_url)) - except UnknownMediaType: - pass return BrowseMedia( title="Music Library", diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 1a9e9ef58df..5e59918650e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1,5 +1,6 @@ """Support to interface with Sonos players.""" import asyncio +from contextlib import suppress import datetime import functools as ft import logging @@ -790,7 +791,7 @@ class SonosEntity(MediaPlayerEntity): coordinator_uid = self.unique_id slave_uids = [] - try: + with suppress(SoCoException): if self.soco.group and self.soco.group.coordinator: coordinator_uid = self.soco.group.coordinator.uid slave_uids = [ @@ -798,8 +799,6 @@ class SonosEntity(MediaPlayerEntity): for p in self.soco.group.members if p.uid != coordinator_uid ] - except SoCoException: - pass return [coordinator_uid] + slave_uids @@ -1009,7 +1008,10 @@ class SonosEntity(MediaPlayerEntity): if len(fav) == 1: src = fav.pop() uri = src.reference.get_uri() - if self.soco.music_source_from_uri(uri) == MUSIC_SRC_RADIO: + if self.soco.music_source_from_uri(uri) in [ + MUSIC_SRC_RADIO, + MUSIC_SRC_LINE_IN, + ]: self.soco.play_uri(uri, title=source) else: self.soco.clear_queue() @@ -1330,7 +1332,7 @@ class SonosEntity(MediaPlayerEntity): if one_alarm._alarm_id == str(alarm_id): alarm = one_alarm if alarm is None: - _LOGGER.warning("did not find alarm with id %s", alarm_id) + _LOGGER.warning("Did not find alarm with id %s", alarm_id) return if time is not None: alarm.start_time = time @@ -1366,7 +1368,7 @@ class SonosEntity(MediaPlayerEntity): self.soco.remove_from_queue(queue_position) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return entity specific state attributes.""" attributes = {ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group]} diff --git a/homeassistant/components/sonos/translations/hu.json b/homeassistant/components/sonos/translations/hu.json index aa10087a884..2123ec520f7 100644 --- a/homeassistant/components/sonos/translations/hu.json +++ b/homeassistant/components/sonos/translations/hu.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Nem tal\u00e1lhat\u00f3k Sonos eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", - "single_instance_allowed": "Csak egyetlen Sonos konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/translations/id.json b/homeassistant/components/sonos/translations/id.json index ef88cab5814..145e2775e4a 100644 --- a/homeassistant/components/sonos/translations/id.json +++ b/homeassistant/components/sonos/translations/id.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "no_devices_found": "Tidak ada perangkat Sonos yang ditemukan pada jaringan.", - "single_instance_allowed": "Hanya satu konfigurasi Sonos yang diperlukan." + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "step": { "confirm": { - "description": "Apakah Anda ingin mengatur Sonos?" + "description": "Ingin menyiapkan Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/ko.json b/homeassistant/components/sonos/translations/ko.json index ba85f8df170..f85f3c5cab4 100644 --- a/homeassistant/components/sonos/translations/ko.json +++ b/homeassistant/components/sonos/translations/ko.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { - "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "Sonos\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/sonos/translations/nl.json b/homeassistant/components/sonos/translations/nl.json index e52111fc50f..42298f0b4f7 100644 --- a/homeassistant/components/sonos/translations/nl.json +++ b/homeassistant/components/sonos/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Geen Sonos-apparaten gevonden op het netwerk.", - "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Sonos nodig." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 723478ac34b..935b33cc5df 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -65,7 +65,7 @@ class SonyProjector(SwitchEntity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return state attributes.""" return self._attributes diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 83c8192ccb2..1b07f01e92a 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -440,7 +440,7 @@ class SoundTouchDevice(MediaPlayerEntity): self._device.add_zone_slave([slave.device for slave in slaves]) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return entity specific state attributes.""" attributes = {} diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index 571e0ab62f3..66583050b20 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -1,4 +1,6 @@ """Support for the SpaceAPI.""" +from contextlib import suppress + import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -6,6 +8,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_LOCATION, + ATTR_NAME, ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, @@ -35,7 +38,6 @@ ATTR_CONTACT = "contact" ATTR_ISSUE_REPORT_CHANNELS = "issue_report_channels" ATTR_LASTCHANGE = "lastchange" ATTR_LOGO = "logo" -ATTR_NAME = "name" ATTR_OPEN = "open" ATTR_SENSORS = "sensors" ATTR_SPACE = "space" @@ -287,13 +289,11 @@ class APISpaceApiView(HomeAssistantView): else: state = {ATTR_OPEN: "null", ATTR_LASTCHANGE: 0} - try: + with suppress(KeyError): state[ATTR_ICON] = { ATTR_OPEN: spaceapi["state"][CONF_ICON_OPEN], ATTR_CLOSE: spaceapi["state"][CONF_ICON_CLOSED], } - except KeyError: - pass data = { ATTR_API: SPACEAPI_VERSION, @@ -306,40 +306,26 @@ class APISpaceApiView(HomeAssistantView): ATTR_URL: spaceapi[CONF_URL], } - try: + with suppress(KeyError): data[ATTR_CAM] = spaceapi[CONF_CAM] - except KeyError: - pass - try: + with suppress(KeyError): data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED] - except KeyError: - pass - try: + with suppress(KeyError): data[ATTR_STREAM] = spaceapi[CONF_STREAM] - except KeyError: - pass - try: + with suppress(KeyError): data[ATTR_FEEDS] = spaceapi[CONF_FEEDS] - except KeyError: - pass - try: + with suppress(KeyError): data[ATTR_CACHE] = spaceapi[CONF_CACHE] - except KeyError: - pass - try: + with suppress(KeyError): data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS] - except KeyError: - pass - try: + with suppress(KeyError): data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW] - except KeyError: - pass if is_sensors is not None: sensors = {} diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 5ff405b59a8..84d63ebc33b 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -13,8 +13,8 @@ from .const import ( DEFAULT_NAME, DEFAULT_SCAN_INTERVAL, DEFAULT_SERVER, + DOMAIN, ) -from .const import DOMAIN # pylint: disable=unused-import class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 5607d2570c9..c49a5691cec 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -1,4 +1,5 @@ """Support for Speedtest.net internet speed testing sensor.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity @@ -30,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class SpeedtestSensor(CoordinatorEntity, RestoreEntity): +class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): """Implementation of a speedtest.net sensor.""" def __init__(self, coordinator, sensor_type): @@ -67,7 +68,7 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not self.coordinator.data: return None diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 3b5ef0b26e1..f2635c19f03 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" } } }, diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json new file mode 100644 index 00000000000..ec08c711e1d --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "wrong_server_id": "A szerver azonos\u00edt\u00f3 \u00e9rv\u00e9nytelen" + }, + "step": { + "user": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "server_name": "V\u00e1laszd ki a teszt szervert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json new file mode 100644 index 00000000000..24e78609380 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "wrong_server_id": "ID server tidak valid" + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "Nonaktifkan pembaruan otomatis", + "scan_interval": "Frekuensi pembaruan (menit)", + "server_name": "Pilih server uji" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ko.json b/homeassistant/components/speedtestdotnet/translations/ko.json index 2951d72d201..e3b65208317 100644 --- a/homeassistant/components/speedtestdotnet/translations/ko.json +++ b/homeassistant/components/speedtestdotnet/translations/ko.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "wrong_server_id": "\uc11c\ubc84 ID \uac00 \uc798\ubabb\ub418\uc5c8\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.", + "wrong_server_id": "\uc11c\ubc84 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 1fe99195f7a..5de8460fd77 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -9,5 +9,16 @@ "description": "Wil je beginnen met instellen?" } } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "Automatische updaten uitschakelen", + "scan_interval": "Update frequentie (minuten)", + "server_name": "Selecteer testserver" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index b0c34ae5a08..d9ccdfd248a 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -66,9 +66,9 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][entry.entry_id] = api - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -79,8 +79,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/spider/translations/hu.json b/homeassistant/components/spider/translations/hu.json new file mode 100644 index 00000000000..9639cfe6367 --- /dev/null +++ b/homeassistant/components/spider/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Bejelentkez\u00e9s mijn.ithodaalderop.nl fi\u00f3kkal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/id.json b/homeassistant/components/spider/translations/id.json new file mode 100644 index 00000000000..2ea038fdcdd --- /dev/null +++ b/homeassistant/components/spider/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Masuk dengan akun mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/ko.json b/homeassistant/components/spider/translations/ko.json index 9e9ed5b0f30..5c72b30726a 100644 --- a/homeassistant/components/spider/translations/ko.json +++ b/homeassistant/components/spider/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -12,7 +12,8 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "title": "mijn.ithodaalderop.nl \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778\ud558\uae30" } } } diff --git a/homeassistant/components/spider/translations/nl.json b/homeassistant/components/spider/translations/nl.json index bc7683ac0a4..373d203aed7 100644 --- a/homeassistant/components/spider/translations/nl.json +++ b/homeassistant/components/spider/translations/nl.json @@ -12,7 +12,8 @@ "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "title": "Aanmelden met mijn.ithodaalderop.nl account" } } } diff --git a/homeassistant/components/spider/translations/ru.json b/homeassistant/components/spider/translations/ru.json index 1b1a175cce5..08d70e44065 100644 --- a/homeassistant/components/spider/translations/ru.json +++ b/homeassistant/components/spider/translations/ru.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 mijn.ithodaalderop.nl" } diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py index 30aa80b5e7d..72a6fec84e9 100644 --- a/homeassistant/components/spotcrime/sensor.py +++ b/homeassistant/components/spotcrime/sensor.py @@ -6,7 +6,7 @@ from datetime import timedelta import spotcrime import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_RADIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify CONF_DAYS = "days" @@ -66,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class SpotCrimeSensor(Entity): +class SpotCrimeSensor(SensorEntity): """Representation of a Spot Crime Sensor.""" def __init__( @@ -103,7 +102,7 @@ class SpotCrimeSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index e28e1fcf315..e36491670f5 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.spotify import config_flow -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -87,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=entry.data, ) ) diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index afad75f0f39..d0fb73e18bd 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Spotify.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from spotipy import Spotify import voluptuous as vol @@ -24,7 +26,7 @@ class SpotifyFlowHandler( def __init__(self) -> None: """Instantiate config flow.""" super().__init__() - self.entry: Optional[Dict[str, Any]] = None + self.entry: dict[str, Any] | None = None @property def logger(self) -> logging.Logger: @@ -32,11 +34,11 @@ class SpotifyFlowHandler( return logging.getLogger(__name__) @property - def extra_authorize_data(self) -> Dict[str, Any]: + def extra_authorize_data(self) -> dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" return {"scope": ",".join(SPOTIFY_SCOPES)} - async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any]: """Create an entry for Spotify.""" spotify = Spotify(auth=data["token"]["access_token"]) @@ -58,7 +60,7 @@ class SpotifyFlowHandler( return self.async_create_entry(title=name, data=data) - async def async_step_reauth(self, entry: Dict[str, Any]) -> Dict[str, Any]: + async def async_step_reauth(self, entry: dict[str, Any]) -> dict[str, Any]: """Perform reauth upon migration of old entries.""" if entry: self.entry = entry @@ -73,8 +75,8 @@ class SpotifyFlowHandler( return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index c4d7378c060..bd92217e9cf 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.16.1"], + "requirements": ["spotipy==2.17.1"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e4450e7a306..0a291582a30 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -1,9 +1,11 @@ """Support for interacting with Spotify Connect.""" +from __future__ import annotations + from asyncio import run_coroutine_threadsafe import datetime as dt from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable import requests from spotipy import Spotify, SpotifyException @@ -50,6 +52,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp @@ -185,7 +188,7 @@ class UnknownMediaType(BrowseError): async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Spotify based on a config entry.""" spotify = SpotifyMediaPlayer( @@ -210,8 +213,12 @@ def spotify_exception_handler(func): result = func(self, *args, **kwargs) self.player_available = True return result - except (SpotifyException, requests.RequestException): + except requests.RequestException: self.player_available = False + except SpotifyException as exc: + self.player_available = False + if exc.reason == "NO_ACTIVE_DEVICE": + raise HomeAssistantError("No active playback device found") from None return wrapper @@ -237,9 +244,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): SPOTIFY_SCOPES ) - self._currently_playing: Optional[dict] = {} - self._devices: Optional[List[dict]] = [] - self._playlist: Optional[dict] = None + self._currently_playing: dict | None = {} + self._devices: list[dict] | None = [] + self._playlist: dict | None = None self._spotify: Spotify = None self.player_available = False @@ -265,7 +272,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return self._id @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" if self._me is not None: model = self._me["product"] @@ -278,7 +285,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): } @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the playback state.""" if not self._currently_playing: return STATE_IDLE @@ -287,44 +294,44 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return STATE_PAUSED @property - def volume_level(self) -> Optional[float]: + def volume_level(self) -> float | None: """Return the device volume.""" return self._currently_playing.get("device", {}).get("volume_percent", 0) / 100 @property - def media_content_id(self) -> Optional[str]: + def media_content_id(self) -> str | None: """Return the media URL.""" item = self._currently_playing.get("item") or {} return item.get("uri") @property - def media_content_type(self) -> Optional[str]: + def media_content_type(self) -> str | None: """Return the media type.""" return MEDIA_TYPE_MUSIC @property - def media_duration(self) -> Optional[int]: + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["duration_ms"] / 1000 @property - def media_position(self) -> Optional[str]: + def media_position(self) -> str | None: """Position of current playing media in seconds.""" if not self._currently_playing: return None return self._currently_playing["progress_ms"] / 1000 @property - def media_position_updated_at(self) -> Optional[dt.datetime]: + def media_position_updated_at(self) -> dt.datetime | None: """When was the position of the current playing media valid.""" if not self._currently_playing: return None return utc_from_timestamp(self._currently_playing["timestamp"] / 1000) @property - def media_image_url(self) -> Optional[str]: + def media_image_url(self) -> str | None: """Return the media image URL.""" if ( self._currently_playing.get("item") is None @@ -339,13 +346,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return False @property - def media_title(self) -> Optional[str]: + def media_title(self) -> str | None: """Return the media title.""" item = self._currently_playing.get("item") or {} return item.get("name") @property - def media_artist(self) -> Optional[str]: + def media_artist(self) -> str | None: """Return the media artist.""" if self._currently_playing.get("item") is None: return None @@ -354,14 +361,14 @@ class SpotifyMediaPlayer(MediaPlayerEntity): ) @property - def media_album_name(self) -> Optional[str]: + def media_album_name(self) -> str | None: """Return the media album.""" if self._currently_playing.get("item") is None: return None return self._currently_playing["item"]["album"]["name"] @property - def media_track(self) -> Optional[int]: + def media_track(self) -> int | None: """Track number of current playing media, music track only.""" item = self._currently_playing.get("item") or {} return item.get("track_number") @@ -374,12 +381,12 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return self._playlist["name"] @property - def source(self) -> Optional[str]: + def source(self) -> str | None: """Return the current playback device.""" return self._currently_playing.get("device", {}).get("name") @property - def source_list(self) -> Optional[List[str]]: + def source_list(self) -> list[str] | None: """Return a list of source devices.""" if not self._devices: return None @@ -391,7 +398,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return bool(self._currently_playing.get("shuffle_state")) @property - def repeat(self) -> Optional[str]: + def repeat(self) -> str | None: """Return current repeat mode.""" repeat_state = self._currently_playing.get("repeat_state") return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) @@ -621,7 +628,7 @@ def build_item_response(spotify, user, payload): try: item_id = item["id"] except KeyError: - _LOGGER.debug("Missing id for media item: %s", item) + _LOGGER.debug("Missing ID for media item: %s", item) continue media_item.children.append( BrowseMedia( @@ -677,7 +684,7 @@ def item_payload(item): media_type = item["type"] media_id = item["uri"] except KeyError as err: - _LOGGER.debug("Missing type or uri for media item: %s", item) + _LOGGER.debug("Missing type or URI for media item: %s", item) raise MissingMediaInformation from err try: diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index fb0dc0f8a1f..060aeffe8bd 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -2,15 +2,24 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." + "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, "create_entry": { "default": "A Spotify sikeresen hiteles\u00edtett." }, "step": { "pick_implementation": { - "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "A Spotify API v\u00e9gpont el\u00e9rhet\u0151" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/id.json b/homeassistant/components/spotify/translations/id.json new file mode 100644 index 00000000000..f75f4159a96 --- /dev/null +++ b/homeassistant/components/spotify/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Integrasi Spotify tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "reauth_account_mismatch": "Akun Spotify yang digunakan untuk autentikasi tidak cocok dengan akun yang memerlukan autentikasi ulang." + }, + "create_entry": { + "default": "Berhasil mengautentikasi dengan Spotify." + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Spotify perlu diautentikasi ulang ke Spotify untuk akun: {account}", + "title": "Autentikasi Ulang Integrasi" + } + } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Titik akhir API Spotify dapat dijangkau" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ko.json b/homeassistant/components/spotify/translations/ko.json index 22a338a8d7c..926231a1de3 100644 --- a/homeassistant/components/spotify/translations/ko.json +++ b/homeassistant/components/spotify/translations/ko.json @@ -4,19 +4,24 @@ "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "reauth_account_mismatch": "\uc778\uc99d\ub41c Spotify \uacc4\uc815\uc740 \uc7ac\uc778\uc99d\uc774 \ud544\uc694\ud55c \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + "reauth_account_mismatch": "\uc778\uc99d \ubc1b\uc740 Spotify \uacc4\uc815\uc774 \uc7ac\uc778\uc99d\ud574\uc57c \ud558\ub294 \uacc4\uc815\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "create_entry": { - "default": "Spotify \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "Spotify\ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, "reauth_confirm": { - "description": "Spotify \ud1b5\ud569\uc740 \uacc4\uc815 {account} \ub300\ud574 Spotify\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4.", - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + "description": "Spotify \uad6c\uc131\uc694\uc18c\ub294 {account} \uacc4\uc815\uc5d0 \ub300\ud574 Spotify\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API \uc5d4\ub4dc \ud3ec\uc778\ud2b8 \uc5f0\uacb0" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 46b18857fe8..0d1e63fde5d 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -11,12 +11,17 @@ }, "step": { "pick_implementation": { - "title": "Kies Authenticatiemethode" + "title": "Kies een authenticatie methode" }, "reauth_confirm": { "description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}", "title": "Verifieer de integratie opnieuw" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API-eindpunt is bereikbaar" + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 670f5e66146..b90ce2f8e59 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -2,16 +2,16 @@ import datetime import decimal import logging +import re import sqlalchemy from sqlalchemy.orm import scoped_session, sessionmaker import voluptuous as vol from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -19,6 +19,13 @@ CONF_COLUMN_NAME = "column" CONF_QUERIES = "queries" CONF_QUERY = "query" +DB_URL_RE = re.compile("//.*:.*@") + + +def redact_credentials(data): + """Redact credentials from string data.""" + return DB_URL_RE.sub("//****:****@", data) + def validate_sql_select(value): """Validate that value is a SQL SELECT query.""" @@ -48,6 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not db_url: db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + sess = None try: engine = sqlalchemy.create_engine(db_url) sessmaker = scoped_session(sessionmaker(bind=engine)) @@ -57,10 +65,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sess.execute("SELECT 1;") except sqlalchemy.exc.SQLAlchemyError as err: - _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) + _LOGGER.error( + "Couldn't connect using %s DB_URL: %s", + redact_credentials(db_url), + redact_credentials(str(err)), + ) return finally: - sess.close() + if sess: + sess.close() queries = [] @@ -90,7 +103,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(queries, True) -class SQLSensor(Entity): +class SQLSensor(SensorEntity): """Representation of an SQL sensor.""" def __init__(self, name, sessmaker, query, column, unit, value_template): @@ -120,7 +133,7 @@ class SQLSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes @@ -148,7 +161,11 @@ class SQLSensor(Entity): value = str(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: - _LOGGER.error("Error executing query %s: %s", self._query, err) + _LOGGER.error( + "Error executing query %s: %s", + self._query, + redact_credentials(str(err)), + ) return finally: sess.close() diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index e298bee7b07..f276daac56a 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -11,11 +11,6 @@ from .const import DISCOVERY_TASK, DOMAIN, PLAYER_DISCOVERY_UNSUB _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Logitech Squeezebox component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Logitech Squeezebox from a config entry.""" hass.async_create_task( diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 9edff5f9a2a..adfa5895b7d 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -15,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession -# pylint: disable=unused-import from .const import DEFAULT_PORT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -80,7 +79,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.discovery_info = { CONF_HOST: server.host, - CONF_PORT: server.port, + CONF_PORT: int(server.port), "uuid": server.uuid, } _LOGGER.debug("Discovered server: %s", self.discovery_info) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index b87695dd159..c57f95266ff 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -265,7 +265,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): self._remove_dispatcher = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device-specific attributes.""" squeezebox_attr = { attr: getattr(self, attr) diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index 742210f3dc6..cdbc5c1426a 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -8,6 +8,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Logitech Squeezebox", "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json index 3b2d79a34a7..216badd15c6 100644 --- a/homeassistant/components/squeezebox/translations/hu.json +++ b/homeassistant/components/squeezebox/translations/hu.json @@ -1,7 +1,30 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_server_found": "Nem tal\u00e1lhat\u00f3 LMS szerver." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_server_found": "Nem siker\u00fclt automatikusan felfedezni a szervert.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "host": "Hoszt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/id.json b/homeassistant/components/squeezebox/translations/id.json new file mode 100644 index 00000000000..764c356ba84 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/id.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_server_found": "Tidak ada server LMS yang ditemukan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "no_server_found": "Tidak dapat menemukan server secara otomatis.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "title": "Edit informasi koneksi" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/nl.json b/homeassistant/components/squeezebox/translations/nl.json index d023a3e96d1..f60fc640db2 100644 --- a/homeassistant/components/squeezebox/translations/nl.json +++ b/homeassistant/components/squeezebox/translations/nl.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "no_server_found": "Geen LMS server gevonden." }, "error": { "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie", + "no_server_found": "Kan server niet automatisch vinden.", "unknown": "Onverwachte fout" }, "flow_title": "Logitech Squeezebox: {host}", diff --git a/homeassistant/components/squeezebox/translations/ru.json b/homeassistant/components/squeezebox/translations/ru.json index fb07471d116..3b144adc04e 100644 --- a/homeassistant/components/squeezebox/translations/ru.json +++ b/homeassistant/components/squeezebox/translations/ru.json @@ -17,7 +17,7 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438" }, diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py index b65b93e0108..51b6f80bb33 100644 --- a/homeassistant/components/srp_energy/config_flow.py +++ b/homeassistant/components/srp_energy/config_flow.py @@ -7,11 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from .const import ( # pylint:disable=unused-import - CONF_IS_TOU, - DEFAULT_NAME, - SRP_ENERGY_DOMAIN, -) +from .const import CONF_IS_TOU, DEFAULT_NAME, SRP_ENERGY_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 36a8798b05b..6973c58600e 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -5,8 +5,8 @@ import logging import async_timeout from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR -from homeassistant.helpers import entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -71,7 +71,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities([SrpEntity(coordinator)]) -class SrpEntity(entity.Entity): +class SrpEntity(SensorEntity): """Implementation of a Srp Energy Usage sensor.""" def __init__(self, coordinator): @@ -122,7 +122,7 @@ class SrpEntity(entity.Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if not self.coordinator.data: return None diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json index f46e17923ad..0c3bdf29389 100644 --- a/homeassistant/components/srp_energy/translations/hu.json +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "user": { "data": { - "id": "A fi\u00f3k azonos\u00edt\u00f3ja" + "id": "A fi\u00f3k azonos\u00edt\u00f3ja", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } - } + }, + "title": "SRP Energy" } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/id.json b/homeassistant/components/srp_energy/translations/id.json new file mode 100644 index 00000000000..fefcbff2ecb --- /dev/null +++ b/homeassistant/components/srp_energy/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_account": "ID akun harus terdiri dari 9 angka", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "id": "ID akun", + "is_tou": "Dalam Paket Time of Use", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ko.json b/homeassistant/components/srp_energy/translations/ko.json index 4b6af62638a..b329cf1f2b1 100644 --- a/homeassistant/components/srp_energy/translations/ko.json +++ b/homeassistant/components/srp_energy/translations/ko.json @@ -1,20 +1,24 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_account": "\uacc4\uc815 ID\ub294 9\uc790\ub9ac \uc22b\uc790\uc5ec\uc57c \ud569\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": { + "id": "\uacc4\uc815 ID", + "is_tou": "\uacc4\uc2dc\ubcc4 \uc694\uae08\uc81c", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } } } - } + }, + "title": "SRP Energy" } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/nl.json b/homeassistant/components/srp_energy/translations/nl.json index cd06c36b661..91bdc3592b6 100644 --- a/homeassistant/components/srp_energy/translations/nl.json +++ b/homeassistant/components/srp_energy/translations/nl.json @@ -12,6 +12,8 @@ "step": { "user": { "data": { + "id": "Account ID", + "is_tou": "Is tijd van gebruik plan", "password": "Wachtwoord", "username": "Gebruikersnaam" } diff --git a/homeassistant/components/srp_energy/translations/ru.json b/homeassistant/components/srp_energy/translations/ru.json index 125f3a5addc..a492fa7dfb2 100644 --- a/homeassistant/components/srp_energy/translations/ru.json +++ b/homeassistant/components/srp_energy/translations/ru.json @@ -15,7 +15,7 @@ "id": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430", "is_tou": "\u041f\u043b\u0430\u043d \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 931119e2398..938ad979daf 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.14.13"], + "requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.16.0"], "after_dependencies": ["zeroconf"], "codeowners": [] } diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 3025a7b4c11..2eb729721d3 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL -from homeassistant.core import Config, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .account import StarlineAccount @@ -19,11 +19,6 @@ from .const import ( ) -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up configured StarLine.""" - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the StarLine device from a config entry.""" account = StarlineAccount(hass, config_entry) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 7452253019b..3f82b816cd5 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -1,6 +1,8 @@ """StarLine Account.""" +from __future__ import annotations + from datetime import datetime, timedelta -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable from starline import StarlineApi, StarlineDevice @@ -29,8 +31,8 @@ class StarlineAccount: self._config_entry: ConfigEntry = config_entry self._update_interval: int = DEFAULT_SCAN_INTERVAL self._update_obd_interval: int = DEFAULT_SCAN_OBD_INTERVAL - self._unsubscribe_auto_updater: Optional[Callable] = None - self._unsubscribe_auto_obd_updater: Optional[Callable] = None + self._unsubscribe_auto_updater: Callable | None = None + self._unsubscribe_auto_obd_updater: Callable | None = None self._api: StarlineApi = StarlineApi( config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN] ) @@ -114,7 +116,7 @@ class StarlineAccount: def unload(self): """Unload StarLine API.""" - _LOGGER.debug("Unloading StarLine API.") + _LOGGER.debug("Unloading StarLine API") if self._unsubscribe_auto_updater is not None: self._unsubscribe_auto_updater() self._unsubscribe_auto_updater = None @@ -123,7 +125,7 @@ class StarlineAccount: self._unsubscribe_auto_obd_updater = None @staticmethod - def device_info(device: StarlineDevice) -> Dict[str, Any]: + def device_info(device: StarlineDevice) -> dict[str, Any]: """Device information for entities.""" return { "identifiers": {(DOMAIN, device.device_id)}, @@ -134,7 +136,7 @@ class StarlineAccount: } @staticmethod - def gps_attrs(device: StarlineDevice) -> Dict[str, Any]: + def gps_attrs(device: StarlineDevice) -> dict[str, Any]: """Attributes for device tracker.""" return { "updated": datetime.utcfromtimestamp(device.position["ts"]).isoformat(), @@ -142,7 +144,7 @@ class StarlineAccount: } @staticmethod - def balance_attrs(device: StarlineDevice) -> Dict[str, Any]: + def balance_attrs(device: StarlineDevice) -> dict[str, Any]: """Attributes for balance sensor.""" return { "operator": device.balance.get("operator"), @@ -151,7 +153,7 @@ class StarlineAccount: } @staticmethod - def gsm_attrs(device: StarlineDevice) -> Dict[str, Any]: + def gsm_attrs(device: StarlineDevice) -> dict[str, Any]: """Attributes for GSM sensor.""" return { "raw": device.gsm_level, @@ -161,7 +163,7 @@ class StarlineAccount: } @staticmethod - def engine_attrs(device: StarlineDevice) -> Dict[str, Any]: + def engine_attrs(device: StarlineDevice) -> dict[str, Any]: """Attributes for engine switch.""" return { "autostart": device.car_state.get("r_start"), @@ -169,6 +171,6 @@ class StarlineAccount: } @staticmethod - def errors_attrs(device: StarlineDevice) -> Dict[str, Any]: + def errors_attrs(device: StarlineDevice) -> dict[str, Any]: """Attributes for errors sensor.""" return {"errors": device.errors.get("errors")} diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index d6e8d6f98ea..9f8e0339210 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure StarLine component.""" -from typing import Optional +from __future__ import annotations from starline import StarlineAuth import voluptuous as vol @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import ( # pylint: disable=unused-import +from .const import ( _LOGGER, CONF_APP_ID, CONF_APP_SECRET, @@ -32,11 +32,11 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" - self._app_id: Optional[str] = None - self._app_secret: Optional[str] = None - self._username: Optional[str] = None - self._password: Optional[str] = None - self._mfa_code: Optional[str] = None + self._app_id: str | None = None + self._app_secret: str | None = None + self._username: str | None = None + self._password: str | None = None + self._mfa_code: str | None = None self._app_code = None self._app_token = None diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 6f202bbae52..59b9f5b4f95 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -26,7 +26,7 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): super().__init__(account, device, "location", "Location") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific attributes.""" return self._account.gps_attrs(self._device) diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index 5db4d369f5e..9b81481b9d1 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -1,5 +1,7 @@ """StarLine base entity.""" -from typing import Callable, Optional +from __future__ import annotations + +from typing import Callable from homeassistant.helpers.entity import Entity @@ -17,7 +19,7 @@ class StarlineEntity(Entity): self._device = device self._key = key self._name = name - self._unsubscribe_api: Optional[Callable] = None + self._unsubscribe_api: Callable | None = None @property def should_poll(self): diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 0b158451fb3..f19fa4896ba 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -31,7 +31,7 @@ class StarlineLock(StarlineEntity, LockEntity): return super().available and self._device.online @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the lock. Possible dictionary keys: diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 8aba1b54269..29deacee428 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,5 +1,5 @@ """Reads vehicle status from StarLine API.""" -from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE, SensorEntity from homeassistant.const import ( LENGTH_KILOMETERS, PERCENTAGE, @@ -7,7 +7,6 @@ from homeassistant.const import ( VOLT, VOLUME_LITERS, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level from .account import StarlineAccount, StarlineDevice @@ -38,7 +37,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class StarlineSensor(StarlineEntity, Entity): +class StarlineSensor(StarlineEntity, SensorEntity): """Representation of a StarLine sensor.""" def __init__( @@ -109,7 +108,7 @@ class StarlineSensor(StarlineEntity, Entity): return self._device_class @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._key == "balance": return self._account.balance_attrs(self._device) diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index c50a7bb4973..b3214390a44 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -53,7 +53,7 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): return super().available and self._device.online @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the switch.""" if self._key == "ign": return self._account.engine_attrs(self._device) diff --git a/homeassistant/components/starline/translations/he.json b/homeassistant/components/starline/translations/he.json new file mode 100644 index 00000000000..53542798232 --- /dev/null +++ b/homeassistant/components/starline/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "auth_user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/hu.json b/homeassistant/components/starline/translations/hu.json index 9d544eb0337..71895e80ad5 100644 --- a/homeassistant/components/starline/translations/hu.json +++ b/homeassistant/components/starline/translations/hu.json @@ -11,7 +11,7 @@ "app_id": "App ID", "app_secret": "Titok" }, - "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a StarLine fejleszt\u0151i fi\u00f3kb\u00f3l ", + "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a [StarLine fejleszt\u0151i fi\u00f3kb\u00f3l](https://my.starline.ru/developer)", "title": "Alkalmaz\u00e1si hiteles\u00edt\u0151 adatok" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/id.json b/homeassistant/components/starline/translations/id.json new file mode 100644 index 00000000000..5a0afdba7ef --- /dev/null +++ b/homeassistant/components/starline/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "error_auth_app": "ID aplikasi atau kode rahasia salah", + "error_auth_mfa": "Kode salah", + "error_auth_user": "Nama pengguna atau kata sandi salah" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID Aplikasi", + "app_secret": "Kode Rahasia" + }, + "description": "ID Aplikasi dan kode rahasia dari [akun pengembang StarLine] (https://my.starline.ru/developer)", + "title": "Kredensial aplikasi" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode dari gambar" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Kode SMS" + }, + "description": "Masukkan kode yang dikirimkan ke ponsel {phone_number}", + "title": "Autentikasi Dua Faktor" + }, + "auth_user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Email dan kata sandi akun StarLine", + "title": "Kredensial pengguna" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/ru.json b/homeassistant/components/starline/translations/ru.json index ea6833f5842..a89fc3b15d5 100644 --- a/homeassistant/components/starline/translations/ru.json +++ b/homeassistant/components/starline/translations/ru.json @@ -3,7 +3,7 @@ "error": { "error_auth_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434.", "error_auth_mfa": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.", - "error_auth_user": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + "error_auth_user": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "auth_app": { @@ -31,7 +31,7 @@ "auth_user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 StarLine", "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" diff --git a/homeassistant/components/starline/translations/zh-Hant.json b/homeassistant/components/starline/translations/zh-Hant.json index 81a65ac0405..722c5daaad4 100644 --- a/homeassistant/components/starline/translations/zh-Hant.json +++ b/homeassistant/components/starline/translations/zh-Hant.json @@ -26,7 +26,7 @@ "mfa_code": "\u7c21\u8a0a\u5bc6\u78bc" }, "description": "\u8f38\u5165\u50b3\u9001\u81f3 {phone_number} \u7684\u9a57\u8b49\u78bc", - "title": "\u96d9\u91cd\u9a57\u8b49" + "title": "\u96d9\u91cd\u8a8d\u8b49" }, "auth_user": { "data": { diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 20fa646ce41..77f5ab307cb 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -5,10 +5,9 @@ import requests from starlingbank import StarlingAccount import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -62,7 +61,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors, True) -class StarlingBalanceSensor(Entity): +class StarlingBalanceSensor(SensorEntity): """Representation of a Starling balance sensor.""" def __init__(self, starling_account, account_name, balance_data_type): diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index f6867d83212..661e00ed494 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -7,7 +7,7 @@ import async_timeout import voluptuous as vol import xmltodict -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_API_KEY, CONF_MONITORED_VARIABLES, @@ -18,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -75,7 +74,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, True) -class StartcaSensor(Entity): +class StartcaSensor(SensorEntity): """Representation of Start.ca Bandwidth sensor.""" def __init__(self, startcadata, sensor_type, name): diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 11cddc88c87..e32ae0debaf 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_ENTITY_ID, @@ -18,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change_event, @@ -85,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return True -class StatisticsSensor(Entity): +class StatisticsSensor(SensorEntity): """Representation of a Statistics sensor.""" def __init__(self, entity_id, name, sampling_size, max_age, precision): @@ -184,7 +183,7 @@ class StatisticsSensor(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if not self.is_binary: return { diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index dbe83177537..45ae1a6c70a 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -6,11 +6,10 @@ from time import mktime import steam import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval from homeassistant.util.dt import utc_from_timestamp @@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): track_time_interval(hass, do_update, BASE_INTERVAL) -class SteamSensor(Entity): +class SteamSensor(SensorEntity): """A class for the Steam account.""" def __init__(self, account, steamod): @@ -194,7 +193,7 @@ class SteamSensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attr = {} if self._game is not None: diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index d8c32575b17..5ae7a9230f7 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -96,7 +96,7 @@ class StiebelEltron(ClimateEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return {"filter_alarm": self._filter_alarm} diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index a1c36e9a10e..033af78560c 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -57,7 +57,7 @@ class StookalertBinarySensor(BinarySensorEntity): self._api_handler = api_handler @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attribute(s) of the sensor.""" state_attr = {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2d115c6978d..0226bb82f6d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -15,6 +15,7 @@ tokens are expired. Alternatively, a Stream can be configured with keepalive to always keep workers active. """ import logging +import re import secrets import threading import time @@ -38,6 +39,8 @@ from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) +STREAM_SOURCE_RE = re.compile("//(.*):(.*)@") + def create_stream(hass, stream_source, options=None): """Create a stream with the specified identfier based on the source url. @@ -157,7 +160,7 @@ class Stream: def check_idle(self): """Reset access token if all providers are idle.""" - if all([p.idle for p in self._outputs.values()]): + if all(p.idle for p in self._outputs.values()): self.access_token = None def start(self): @@ -173,7 +176,9 @@ class Stream: target=self._run_worker, ) self._thread.start() - _LOGGER.info("Started stream: %s", self.source) + _LOGGER.info( + "Started stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source)) + ) def update_source(self, new_source): """Restart the stream with a new stream source.""" @@ -239,7 +244,9 @@ class Stream: self._thread_quit.set() self._thread.join() self._thread = None - _LOGGER.info("Stopped stream: %s", self.source) + _LOGGER.info( + "Stopped stream: %s", STREAM_SOURCE_RE.sub("//", str(self.source)) + ) async def async_record(self, video_path, duration=30, lookback=5): """Make a .mp4 recording from a provided stream.""" @@ -258,6 +265,7 @@ class Stream: recorder.video_path = video_path self.start() + _LOGGER.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback hls = self.outputs().get("hls") diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 17d4516344a..076eb3596d7 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -1,8 +1,10 @@ """Provides core stream functionality.""" +from __future__ import annotations + import asyncio from collections import deque import io -from typing import Any, Callable, List +from typing import Any, Callable from aiohttp import web import attr @@ -104,7 +106,7 @@ class StreamOutput: return self._idle_timer.idle @property - def segments(self) -> List[int]: + def segments(self) -> list[int]: """Return current sequence from segments.""" return [s.sequence for s in self._segments] diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index b2600977971..4909bbf95a3 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -50,9 +50,8 @@ class HlsMasterPlaylistView(StreamView): track = stream.add_provider("hls") stream.start() # Wait for a segment to be ready - if not track.segments: - if not await track.recv(): - return web.HTTPNotFound() + if not track.segments and not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -110,9 +109,8 @@ class HlsPlaylistView(StreamView): track = stream.add_provider("hls") stream.start() # Wait for a segment to be ready - if not track.segments: - if not await track.recv(): - return web.HTTPNotFound() + if not track.segments and not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 19b9e7b2e8a..400b50eae04 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/stream", "requirements": ["av==8.0.3"], "dependencies": ["http"], - "codeowners": ["@hunterjm", "@uvjustin"], + "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal" } diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 0344e220647..01a8ca9ea6b 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,8 +1,10 @@ """Provide functionality to record stream.""" +from __future__ import annotations + import logging import os import threading -from typing import Deque, List +from typing import Deque import av @@ -21,6 +23,11 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: Deque[Segment]): """Handle saving stream.""" + + if not segments: + _LOGGER.error("Recording failed to capture anything") + return + if not os.path.exists(os.path.dirname(file_out)): os.makedirs(os.path.dirname(file_out), exist_ok=True) @@ -110,7 +117,7 @@ class RecorderOutput(StreamOutput): """Return provider name.""" return "recorder" - def prepend(self, segments: List[Segment]) -> None: + def prepend(self, segments: list[Segment]) -> None: """Prepend segments to existing list.""" self._segments.extendleft(reversed(segments)) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index d5760877c43..5a129356983 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,6 +5,7 @@ import logging import av +from . import STREAM_SOURCE_RE from .const import ( AUDIO_CODECS, MAX_MISSING_DTS, @@ -25,7 +26,7 @@ def create_stream_buffer(video_stream, audio_stream, sequence): segment = io.BytesIO() container_options = { # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont", + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets", "avoid_negative_ts": "disabled", "fragment_index": str(sequence), } @@ -127,7 +128,9 @@ def stream_worker(source, options, segment_buffer, quit_event): try: container = av.open(source, options=options, timeout=STREAM_TIMEOUT) except av.AVError: - _LOGGER.error("Error opening stream %s", source) + _LOGGER.error( + "Error opening stream %s", STREAM_SOURCE_RE.sub("//", str(source)) + ) return try: video_stream = container.streams.video[0] @@ -208,6 +211,16 @@ def stream_worker(source, options, segment_buffer, quit_event): missing_dts += 1 continue if packet.stream == audio_stream: + # detect ADTS AAC and disable audio + if audio_stream.codec.name == "aac" and packet.size > 2: + with memoryview(packet) as packet_view: + if packet_view[0] == 0xFF and packet_view[1] & 0xF0 == 0xF0: + _LOGGER.warning( + "ADTS AAC detected - disabling audio stream" + ) + container_packets = container.demux(video_stream) + audio_stream = None + continue found_audio = True elif ( segment_start_pts is None diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 336e92358ee..0a49e128c2f 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -17,7 +17,7 @@ SERVICE_SET_AWAY_MODE = "set_away_mode" AWAY_MODE_AWAY = "away" AWAY_MODE_HOME = "home" -STREAMLABSWATER_COMPONENTS = ["sensor", "binary_sensor"] +PLATFORMS = ["sensor", "binary_sensor"] CONF_LOCATION_ID = "location_id" @@ -39,7 +39,7 @@ SET_AWAY_MODE_SCHEMA = vol.Schema( def setup(hass, config): - """Set up the streamlabs water component.""" + """Set up the streamlabs water integration.""" conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) @@ -74,8 +74,8 @@ def setup(hass, config): "location_name": location_name, } - for component in STREAMLABSWATER_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) def set_away_mode(service): """Set the StreamLabsWater Away Mode.""" diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index e7168f8ec0b..ba722d0a4f2 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -2,9 +2,9 @@ from datetime import timedelta +from homeassistant.components.sensor import SensorEntity from homeassistant.components.streamlabswater import DOMAIN as STREAMLABSWATER_DOMAIN from homeassistant.const import VOLUME_GALLONS -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle DEPENDENCIES = ["streamlabswater"] @@ -67,7 +67,7 @@ class StreamlabsUsageData: return self._this_year -class StreamLabsDailyUsage(Entity): +class StreamLabsDailyUsage(SensorEntity): """Monitors the daily water usage.""" def __init__(self, location_name, streamlabs_usage_data): diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 0ad621f0707..5c45e5e3d44 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -1,8 +1,9 @@ """Provide functionality to STT.""" +from __future__ import annotations + from abc import ABC, abstractmethod import asyncio import logging -from typing import Dict, List, Optional from aiohttp import StreamReader, web from aiohttp.hdrs import istr @@ -96,44 +97,44 @@ class SpeechMetadata: class SpeechResult: """Result of audio Speech.""" - text: Optional[str] = attr.ib() + text: str | None = attr.ib() result: SpeechResultState = attr.ib() class Provider(ABC): """Represent a single STT provider.""" - hass: Optional[HomeAssistantType] = None - name: Optional[str] = None + hass: HomeAssistantType | None = None + name: str | None = None @property @abstractmethod - def supported_languages(self) -> List[str]: + def supported_languages(self) -> list[str]: """Return a list of supported languages.""" @property @abstractmethod - def supported_formats(self) -> List[AudioFormats]: + def supported_formats(self) -> list[AudioFormats]: """Return a list of supported formats.""" @property @abstractmethod - def supported_codecs(self) -> List[AudioCodecs]: + def supported_codecs(self) -> list[AudioCodecs]: """Return a list of supported codecs.""" @property @abstractmethod - def supported_bit_rates(self) -> List[AudioBitRates]: + def supported_bit_rates(self) -> list[AudioBitRates]: """Return a list of supported bit rates.""" @property @abstractmethod - def supported_sample_rates(self) -> List[AudioSampleRates]: + def supported_sample_rates(self) -> list[AudioSampleRates]: """Return a list of supported sample rates.""" @property @abstractmethod - def supported_channels(self) -> List[AudioChannels]: + def supported_channels(self) -> list[AudioChannels]: """Return a list of supported channels.""" @abstractmethod @@ -167,12 +168,12 @@ class SpeechToTextView(HomeAssistantView): url = "/api/stt/{provider}" name = "api:stt:provider" - def __init__(self, providers: Dict[str, Provider]) -> None: + def __init__(self, providers: dict[str, Provider]) -> None: """Initialize a tts view.""" self.providers = providers @staticmethod - def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]: + def _metadata_from_header(request: web.Request) -> SpeechMetadata | None: """Extract metadata from header. X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 63bc644b50a..04f61111671 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -22,7 +22,7 @@ from .const import ( ENTRY_COORDINATOR, ENTRY_VEHICLES, FETCH_INTERVAL, - SUPPORTED_PLATFORMS, + PLATFORMS, UPDATE_INTERVAL, VEHICLE_API_GEN, VEHICLE_HAS_EV, @@ -94,9 +94,9 @@ async def async_setup_entry(hass, entry): ENTRY_VEHICLES: vehicle_info, } - for component in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -107,8 +107,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in SUPPORTED_PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 4c5c476a402..91ed8ad4214 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_US from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -# pylint: disable=unused-import from .const import CONF_COUNTRY, CONF_UPDATE_ENABLED, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,8 +27,11 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - config_data = {CONF_PIN: None} - controller = None + + def __init__(self): + """Initialize config flow.""" + self.config_data = {CONF_PIN: None} + self.controller = None async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" @@ -105,9 +107,8 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device_name=device_name, country=data[CONF_COUNTRY], ) - _LOGGER.debug("Using subarulink %s", self.controller.version) _LOGGER.debug( - "Setting up first time connection to Subuaru API. This may take up to 20 seconds." + "Setting up first time connection to Subaru API. This may take up to 20 seconds" ) if await self.controller.connect(): _LOGGER.debug("Successfully authenticated and authorized with Subaru API") @@ -116,21 +117,20 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pin(self, user_input=None): """Handle second part of config flow, if required.""" error = None - if user_input: - if self.controller.update_saved_pin(user_input[CONF_PIN]): - try: - vol.Match(r"[0-9]{4}")(user_input[CONF_PIN]) - await self.controller.test_pin() - except vol.Invalid: - error = {"base": "bad_pin_format"} - except InvalidPIN: - error = {"base": "incorrect_pin"} - else: - _LOGGER.debug("PIN successfully tested") - self.config_data.update(user_input) - return self.async_create_entry( - title=self.config_data[CONF_USERNAME], data=self.config_data - ) + if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]): + try: + vol.Match(r"[0-9]{4}")(user_input[CONF_PIN]) + await self.controller.test_pin() + except vol.Invalid: + error = {"base": "bad_pin_format"} + except InvalidPIN: + error = {"base": "incorrect_pin"} + else: + _LOGGER.debug("PIN successfully tested") + self.config_data.update(user_input) + return self.async_create_entry( + title=self.config_data[CONF_USERNAME], data=self.config_data + ) return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 7349f9c32d6..cada29edd3a 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -30,18 +30,13 @@ API_GEN_1 = "g1" API_GEN_2 = "g2" MANUFACTURER = "Subaru Corp." -SUPPORTED_PLATFORMS = [ +PLATFORMS = [ "sensor", ] ICONS = { "Avg Fuel Consumption": "mdi:leaf", - "EV Time to Full Charge": "mdi:car-electric", "EV Range": "mdi:ev-station", "Odometer": "mdi:road-variant", "Range": "mdi:gas-station", - "Tire Pressure FL": "mdi:gauge", - "Tire Pressure FR": "mdi:gauge", - "Tire Pressure RL": "mdi:gauge", - "Tire Pressure RR": "mdi:gauge", } diff --git a/homeassistant/components/subaru/entity.py b/homeassistant/components/subaru/entity.py index 4fdeca4e484..559feeea303 100644 --- a/homeassistant/components/subaru/entity.py +++ b/homeassistant/components/subaru/entity.py @@ -1,7 +1,7 @@ """Base class for all Subaru Entities.""" from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, ICONS, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN +from .const import DOMAIN, MANUFACTURER, VEHICLE_NAME, VEHICLE_VIN class SubaruEntity(CoordinatorEntity): @@ -24,11 +24,6 @@ class SubaruEntity(CoordinatorEntity): """Return a unique ID.""" return f"{self.vin}_{self.entity_type}" - @property - def icon(self): - """Return the icon of the sensor.""" - return ICONS.get(self.entity_type) - @property def device_info(self): """Return the device_info of the device.""" diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 594d18028e6..3994c9c6124 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -1,10 +1,12 @@ """Support for Subaru sensors.""" import subarulink.const as sc -from homeassistant.components.sensor import DEVICE_CLASSES +from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, LENGTH_KILOMETERS, LENGTH_MILES, @@ -30,6 +32,7 @@ from .const import ( DOMAIN, ENTRY_COORDINATOR, ENTRY_VEHICLES, + ICONS, VEHICLE_API_GEN, VEHICLE_HAS_EV, VEHICLE_HAS_SAFETY_SERVICE, @@ -76,25 +79,25 @@ API_GEN_2_SENSORS = [ }, { SENSOR_TYPE: "Tire Pressure FL", - SENSOR_CLASS: None, + SENSOR_CLASS: DEVICE_CLASS_PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_FL, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "Tire Pressure FR", - SENSOR_CLASS: None, + SENSOR_CLASS: DEVICE_CLASS_PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_FR, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "Tire Pressure RL", - SENSOR_CLASS: None, + SENSOR_CLASS: DEVICE_CLASS_PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_RL, SENSOR_UNITS: PRESSURE_HPA, }, { SENSOR_TYPE: "Tire Pressure RR", - SENSOR_CLASS: None, + SENSOR_CLASS: DEVICE_CLASS_PRESSURE, SENSOR_FIELD: sc.TIRE_PRESSURE_RR, SENSOR_UNITS: PRESSURE_HPA, }, @@ -128,7 +131,7 @@ EV_SENSORS = [ }, { SENSOR_TYPE: "EV Time to Full Charge", - SENSOR_CLASS: None, + SENSOR_CLASS: DEVICE_CLASS_TIMESTAMP, SENSOR_FIELD: sc.EV_TIME_TO_FULLY_CHARGED, SENSOR_UNITS: TIME_MINUTES, }, @@ -140,7 +143,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = hass.data[DOMAIN][config_entry.entry_id][ENTRY_COORDINATOR] vehicle_info = hass.data[DOMAIN][config_entry.entry_id][ENTRY_VEHICLES] entities = [] - for vin in vehicle_info.keys(): + for vin in vehicle_info: entities.extend(create_vehicle_sensors(vehicle_info[vin], coordinator)) async_add_entities(entities, True) @@ -170,7 +173,7 @@ def create_vehicle_sensors(vehicle_info, coordinator): ] -class SubaruSensor(SubaruEntity): +class SubaruSensor(SubaruEntity, SensorEntity): """Class for Subaru sensors.""" def __init__( @@ -190,7 +193,14 @@ class SubaruSensor(SubaruEntity): """Return the class of this device, from component DEVICE_CLASSES.""" if self.sensor_class in DEVICE_CLASSES: return self.sensor_class - return super().device_class + return None + + @property + def icon(self): + """Return the icon of the sensor.""" + if not self.device_class: + return ICONS.get(self.entity_type) + return None @property def state(self): @@ -210,16 +220,20 @@ class SubaruSensor(SubaruEntity): self.hass.config.units.length(self.current_value, self.api_unit), 1 ) - if self.api_unit in PRESSURE_UNITS: - if self.hass.config.units == IMPERIAL_SYSTEM: - return round( - self.hass.config.units.pressure(self.current_value, self.api_unit), - 1, - ) + if ( + self.api_unit in PRESSURE_UNITS + and self.hass.config.units == IMPERIAL_SYSTEM + ): + return round( + self.hass.config.units.pressure(self.current_value, self.api_unit), + 1, + ) - if self.api_unit in FUEL_CONSUMPTION_UNITS: - if self.hass.config.units == IMPERIAL_SYSTEM: - return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1) + if ( + self.api_unit in FUEL_CONSUMPTION_UNITS + and self.hass.config.units == IMPERIAL_SYSTEM + ): + return round((100.0 * L_PER_GAL) / (KM_PER_MI * self.current_value), 1) return self.current_value diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 064245e0732..ea9df082f3a 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -22,8 +22,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "incorrect_pin": "Incorrect PIN", - "bad_pin_format": "PIN should be 4 digits", - "unknown": "[%key:common::config_flow::error::unknown%]" + "bad_pin_format": "PIN should be 4 digits" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json new file mode 100644 index 00000000000..c3dc8345ecd --- /dev/null +++ b/homeassistant/components/subaru/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "country": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u044a\u0440\u0436\u0430\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index 1c162d61e99..9c4a0bdf535 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -15,13 +15,16 @@ "pin": { "data": { "pin": "PIN" - } + }, + "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben" }, "user": { "data": { + "country": "Land ausw\u00e4hlen", "password": "Passwort", "username": "Benutzername" - } + }, + "description": "Bitte gib deine MySubaru-Anmeldedaten ein\nHINWEIS: Die Ersteinrichtung kann bis zu 30 Sekunden dauern" } } } diff --git a/homeassistant/components/subaru/translations/el.json b/homeassistant/components/subaru/translations/el.json new file mode 100644 index 00000000000..17ede1af4e7 --- /dev/null +++ b/homeassistant/components/subaru/translations/el.json @@ -0,0 +1,35 @@ +{ + "config": { + "error": { + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink" + }, + "user": { + "data": { + "country": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c7\u03ce\u03c1\u03b1\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2 \u03c4\u03bf\u03c5 MySubaru\n\u03a3\u0397\u039c\u0395\u0399\u03a9\u03a3\u0397: \u0397 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03ad\u03c9\u03c2 \u03ba\u03b1\u03b9 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03bf\u03c7\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + }, + "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af, \u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03bf\u03c7\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd \u03b8\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae \u03c3\u03c4\u03bf \u03cc\u03c7\u03b7\u03bc\u03ac \u03c3\u03b1\u03c2 \u03ba\u03ac\u03b8\u03b5 2 \u03ce\u03c1\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b1 \u03bd\u03ad\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03c9\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd. \u03a7\u03c9\u03c1\u03af\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03bf\u03c7\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd, \u03c4\u03b1 \u03bd\u03ad\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03cc\u03c7\u03b7\u03bc\u03b1 \u03c9\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 (\u03ba\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03ac \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03bc\u03b7\u03c7\u03b1\u03bd\u03ce\u03bd).", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json new file mode 100644 index 00000000000..d92ca24b7a1 --- /dev/null +++ b/homeassistant/components/subaru/translations/hu.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "bad_pin_format": "A PIN-nek 4 sz\u00e1mjegy\u0171nek kell lennie", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "incorrect_pin": "Helytelen PIN", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "title": "Subaru Starlink konfigur\u00e1ci\u00f3" + }, + "user": { + "data": { + "country": "V\u00e1lassz orsz\u00e1got", + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Subaru Starlink konfigur\u00e1ci\u00f3" + } + } + }, + "options": { + "step": { + "init": { + "title": "Subaru Starlink be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/id.json b/homeassistant/components/subaru/translations/id.json new file mode 100644 index 00000000000..1ae1506fe09 --- /dev/null +++ b/homeassistant/components/subaru/translations/id.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "bad_pin_format": "PIN harus terdiri dari 4 angka", + "cannot_connect": "Gagal terhubung", + "incorrect_pin": "PIN salah", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "Masukkan PIN MySubaru Anda\nCATATAN: Semua kendaraan dalam akun harus memiliki PIN yang sama", + "title": "Konfigurasi Subaru Starlink" + }, + "user": { + "data": { + "country": "Pilih negara", + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial MySubaru Anda\nCATATAN: Penyiapan awal mungkin memerlukan waktu hingga 30 detik", + "title": "Konfigurasi Subaru Starlink" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Aktifkan polling kendaraan" + }, + "description": "Ketika diaktifkan, polling kendaraan akan mengirim perintah jarak jauh ke kendaraan Anda setiap 2 jam untuk mendapatkan data sensor baru. Tanpa polling kendaraan, data sensor baru hanya diterima ketika kendaraan mengirimkan data secara otomatis (umumnya setelah mesin dimatikan).", + "title": "Opsi Subaru Starlink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/it.json b/homeassistant/components/subaru/translations/it.json index 6dbb0702f46..c585834f12b 100644 --- a/homeassistant/components/subaru/translations/it.json +++ b/homeassistant/components/subaru/translations/it.json @@ -34,9 +34,9 @@ "step": { "init": { "data": { - "update_enabled": "Abilita l'interrogazione del veicolo" + "update_enabled": "Abilita la verifica ciclica del veicolo" }, - "description": "Quando abilitata, l'interrogazione del veicolo invier\u00e0 un comando remoto al tuo veicolo ogni 2 ore per ottenere nuovi dati del sensore. Senza l'interrogazione del veicolo, i nuovi dati del sensore verranno ricevuti solo quando il veicolo invier\u00e0 automaticamente i dati (normalmente dopo lo spegnimento del motore).", + "description": "Quando abilitata, la verifica ciclica del veicolo invier\u00e0 un comando remoto d'interrogazione al tuo veicolo ogni 2 ore per ottenere nuovi dati del sensore. Senza l'interrogazione del veicolo i nuovi dati del sensore verranno ricevuti solo quando il veicolo invier\u00e0 automaticamente i dati (normalmente dopo lo spegnimento del motore).", "title": "Opzioni Subaru Starlink" } } diff --git a/homeassistant/components/subaru/translations/ko.json b/homeassistant/components/subaru/translations/ko.json new file mode 100644 index 00000000000..8fe12309812 --- /dev/null +++ b/homeassistant/components/subaru/translations/ko.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "bad_pin_format": "PIN\uc740 4\uc790\ub9ac\uc5ec\uc57c \ud569\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "incorrect_pin": "PIN\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\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": { + "pin": { + "data": { + "pin": "PIN" + }, + "description": "MySubaru PIN\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694\n\ucc38\uace0: \uacc4\uc815\uc758 \ubaa8\ub4e0 \ucc28\ub7c9\uc740 \ub3d9\uc77c\ud55c PIN \uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", + "title": "Subaru Starlink \uad6c\uc131" + }, + "user": { + "data": { + "country": "\uad6d\uac00 \uc120\ud0dd\ud558\uae30", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "MySubaru \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694\n\ucc38\uace0: \ucd08\uae30 \uc124\uc815\uc5d0\ub294 \ucd5c\ub300 30\ucd08 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4", + "title": "Subaru Starlink \uad6c\uc131" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "\ucc28\ub7c9 \ud3f4\ub9c1 \ud65c\uc131\ud654\ud558\uae30" + }, + "description": "\ud65c\uc131\ud654\ub418\uba74 \ucc28\ub7c9 \ud3f4\ub9c1\uc740 \ucc28\ub7c9\uc5d0 2\uc2dc\uac04\ub9c8\ub2e4 \uc6d0\uaca9 \uba85\ub839\uc744 \uc804\uc1a1\ud558\uc5ec \uc0c8\ub85c\uc6b4 \uc13c\uc11c \ub370\uc774\ud130\ub97c \ubc1b\uc544\uc635\ub2c8\ub2e4. \ucc28\ub7c9 \ud3f4\ub9c1\uc774 \uc5c6\uc73c\uba74 \uc0c8\ub85c\uc6b4 \uc13c\uc11c \ub370\uc774\ud130\ub294 \ucc28\ub7c9\uc774 \uc790\ub3d9\uc73c\ub85c \ub370\uc774\ud130\ub97c \ubcf4\ub0bc \ub54c\ub9cc \uc218\uc2e0\ub429\ub2c8\ub2e4(\uc77c\ubc18\uc801\uc73c\ub85c \uc5d4\uc9c4\uc774 \uaebc\uc9c4 \ud6c4).", + "title": "Subaru Starlink \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/nl.json b/homeassistant/components/subaru/translations/nl.json index 5a9bd4119ff..339c990ca0b 100644 --- a/homeassistant/components/subaru/translations/nl.json +++ b/homeassistant/components/subaru/translations/nl.json @@ -16,6 +16,7 @@ "data": { "pin": "PIN" }, + "description": "Voer uw MySubaru-pincode in\n OPMERKING: Alle voertuigen in een account moeten dezelfde pincode hebben", "title": "Subaru Starlink Configuratie" }, "user": { @@ -24,6 +25,7 @@ "password": "Wachtwoord", "username": "Gebruikersnaam" }, + "description": "Voer uw MySubaru inloggegevens in\nOPMERKING: De eerste installatie kan tot 30 seconden duren", "title": "Subaru Starlink-configuratie" } } @@ -31,6 +33,10 @@ "options": { "step": { "init": { + "data": { + "update_enabled": "Voertuigpeiling inschakelen" + }, + "description": "Wanneer deze optie is ingeschakeld, zal voertuigpeiling om de 2 uur een opdracht op afstand naar uw voertuig sturen om nieuwe sensorgegevens te verkrijgen. Zonder voertuigpeiling worden nieuwe sensorgegevens alleen ontvangen wanneer het voertuig automatisch gegevens doorstuurt (normaal gesproken na het uitschakelen van de motor).", "title": "Subaru Starlink-opties" } } diff --git a/homeassistant/components/subaru/translations/pt.json b/homeassistant/components/subaru/translations/pt.json new file mode 100644 index 00000000000..7f3e1ec8e3b --- /dev/null +++ b/homeassistant/components/subaru/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/ru.json b/homeassistant/components/subaru/translations/ru.json index 7e3fbce6e38..c414ebb385f 100644 --- a/homeassistant/components/subaru/translations/ru.json +++ b/homeassistant/components/subaru/translations/ru.json @@ -23,7 +23,7 @@ "data": { "country": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0443", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "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 MySubaru.\n\u041f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u0434\u043e 30 \u0441\u0435\u043a\u0443\u043d\u0434.", "title": "Subaru Starlink" diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 3bca3484298..7170e0b8a67 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -6,10 +6,9 @@ from pysuez import SuezClient from pysuez.client import PySuezError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) CONF_COUNTER_ID = "counter_id" @@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([SuezSensor(client)], True) -class SuezSensor(Entity): +class SuezSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, client): @@ -73,7 +72,7 @@ class SuezSensor(Entity): return VOLUME_LITERS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 2d921da4a46..dfe3b15c110 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -124,7 +124,7 @@ class Sun(Entity): return STATE_BELOW_HORIZON @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sun.""" return { STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), diff --git a/homeassistant/components/sun/translations/id.json b/homeassistant/components/sun/translations/id.json index 374da4e0db9..df6c960e67d 100644 --- a/homeassistant/components/sun/translations/id.json +++ b/homeassistant/components/sun/translations/id.json @@ -2,7 +2,7 @@ "state": { "_": { "above_horizon": "Terbit", - "below_horizon": "Tenggelam" + "below_horizon": "Terbenam" } }, "title": "Matahari" diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 779d3078f13..d2b6f6de560 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -26,6 +26,7 @@ TRIGGER_SCHEMA = vol.Schema( async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) description = event @@ -44,6 +45,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "event": event, "offset": offset, "description": description, + "id": trigger_id, } }, ) diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index 8e1d6f89eea..f701df2d6c3 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -4,10 +4,9 @@ import xmlrpc.client import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_URL import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -36,7 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class SupervisorProcessSensor(Entity): +class SupervisorProcessSensor(SensorEntity): """Representation of a supervisor-monitored process.""" def __init__(self, info, server): @@ -61,7 +60,7 @@ class SupervisorProcessSensor(Entity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_DESCRIPTION: self._info.get("description"), diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 084811c8fa0..5ebd6d6ca48 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -1,7 +1,8 @@ """Support for Supla devices.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional import async_timeout from asyncpysupla import SuplaAPI @@ -180,7 +181,7 @@ class SuplaChannel(CoordinatorEntity): ) @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the device.""" return self.channel_data["caption"] diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 8ba6809ee05..4a65931d3f0 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -1,6 +1,8 @@ """Support for Sure Petcare cat/pet flaps.""" +from __future__ import annotations + import logging -from typing import Any, Dict, List +from typing import Any from surepy import ( MESTART_RESOURCE, @@ -185,12 +187,12 @@ async def async_setup(hass, config) -> bool: class SurePetcareAPI: """Define a generic Sure Petcare object.""" - def __init__(self, hass, surepy: SurePetcare, ids: List[Dict[str, Any]]) -> None: + def __init__(self, hass, surepy: SurePetcare, ids: list[dict[str, Any]]) -> None: """Initialize the Sure Petcare object.""" self.hass = hass self.surepy = surepy self.ids = ids - self.states: Dict[str, Any] = {} + self.states: dict[str, Any] = {} async def async_update(self, arg: Any = None) -> None: """Refresh Sure Petcare data.""" diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 2a624b580ac..e96a5eaf35e 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -1,7 +1,9 @@ """Support for Sure PetCare Flaps/Pets binary sensors.""" +from __future__ import annotations + from datetime import datetime import logging -from typing import Any, Dict, Optional +from typing import Any from surepy import SureLocationID, SurepyProduct @@ -71,8 +73,8 @@ class SurePetcareBinarySensor(BinarySensorEntity): self._device_class = device_class self._spc: SurePetcareAPI = spc - self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id) - self._state: Dict[str, Any] = {} + self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._state: dict[str, Any] = {} # cover special case where a device has no name set if "name" in self._spc_data: @@ -85,7 +87,7 @@ class SurePetcareBinarySensor(BinarySensorEntity): self._async_unsub_dispatcher_connect = None @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if entity is on/unlocked.""" return bool(self._state) @@ -151,7 +153,7 @@ class Hub(SurePetcareBinarySensor): return self.available @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attributes = None if self._state: @@ -179,7 +181,7 @@ class Pet(SurePetcareBinarySensor): return False @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attributes = None if self._state: @@ -232,7 +234,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): return self.available @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attributes = None if self._state: diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index e2d3d070867..0a49781767b 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -1,9 +1,12 @@ """Support for Sure PetCare Flaps/Pets sensors.""" +from __future__ import annotations + import logging -from typing import Any, Dict, Optional +from typing import Any from surepy import SureLockStateID, SurepyProduct +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_VOLTAGE, CONF_ID, @@ -13,7 +16,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import SurePetcareAPI from .const import ( @@ -52,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities, True) -class SurePetcareSensor(Entity): +class SurePetcareSensor(SensorEntity): """A binary sensor implementation for Sure Petcare Entities.""" def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI): @@ -62,8 +64,8 @@ class SurePetcareSensor(Entity): self._sure_type = sure_type self._spc = spc - self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id) - self._state: Dict[str, Any] = {} + self._spc_data: dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._state: dict[str, Any] = {} self._name = ( f"{self._sure_type.name.capitalize()} " @@ -120,12 +122,12 @@ class Flap(SurePetcareSensor): """Sure Petcare Flap.""" @property - def state(self) -> Optional[int]: + def state(self) -> int | None: """Return battery level in percent.""" return SureLockStateID(self._state["locking"]["mode"]).name.capitalize() @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attributes = None if self._state: @@ -143,9 +145,9 @@ class SureBattery(SurePetcareSensor): return f"{self._name} Battery Level" @property - def state(self) -> Optional[int]: + def state(self) -> int | None: """Return battery level in percent.""" - battery_percent: Optional[int] + battery_percent: int | None try: per_battery_voltage = self._state["battery"] / 4 voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW @@ -166,7 +168,7 @@ class SureBattery(SurePetcareSensor): return DEVICE_CLASS_BATTERY @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" attributes = None if self._state: diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 61423312b2a..47a8d3e5589 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -5,10 +5,9 @@ import logging from swisshydrodata import SwissHydroData import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -83,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -class SwissHydrologicalDataSensor(Entity): +class SwissHydrologicalDataSensor(SensorEntity): """Implementation of a Swiss hydrological sensor.""" def __init__(self, hydro_data, station, condition): @@ -119,7 +118,7 @@ class SwissHydrologicalDataSensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attrs = {} diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 2c7fb483eff..a971524c22b 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -6,11 +6,10 @@ from opendata_transport import OpendataTransport from opendata_transport.exceptions import OpendataTransportError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -68,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([SwissPublicTransportSensor(opendata, start, destination, name)]) -class SwissPublicTransportSensor(Entity): +class SwissPublicTransportSensor(SensorEntity): """Implementation of an Swiss public transport sensor.""" def __init__(self, opendata, start, destination, name): @@ -94,7 +93,7 @@ class SwissPublicTransportSensor(Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._opendata is None: return diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 5662212c9e8..1332aa46189 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -1,4 +1,5 @@ """Support for Swisscom routers (Internet-Box).""" +from contextlib import suppress import logging from aiohttp.hdrs import CONTENT_TYPE @@ -97,13 +98,11 @@ class SwisscomDeviceScanner(DeviceScanner): return devices for device in request.json()["status"]: - try: + with suppress(KeyError, requests.exceptions.RequestException): devices[device["Key"]] = { "ip": device["IPAddress"], "mac": device["PhysAddress"], "host": device["Name"], "status": device["Active"], } - except (KeyError, requests.exceptions.RequestException): - pass return devices diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 1d9b54a0424..c585fdc22d3 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -1,6 +1,7 @@ """Component to interface with switches that can be controlled remotely.""" from datetime import timedelta import logging +from typing import final import voluptuous as vol @@ -79,7 +80,7 @@ async def async_unload_entry(hass, entry): class SwitchEntity(ToggleEntity): - """Representation of a switch.""" + """Base class for switch entities.""" @property def current_power_w(self): @@ -96,6 +97,7 @@ class SwitchEntity(ToggleEntity): """Return true if device is in standby.""" return None + @final @property def state_attributes(self): """Return the optional state attributes.""" diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index a50131f094c..0f3890d329f 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -1,5 +1,5 @@ """Provides device actions for switches.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -25,6 +25,6 @@ async def async_call_action_from_config( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions.""" return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index c928deef01a..15c2e54d193 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for switches.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ def async_condition_from_config( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index cb5d5f7aa0e..15b700d9eb5 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -1,5 +1,5 @@ """Provides device triggers for switches.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ async def async_attach_trigger( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/group.py b/homeassistant/components/switch/group.py index 1636054663d..234883ffd5a 100644 --- a/homeassistant/components/switch/group.py +++ b/homeassistant/components/switch/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/switch/light.py b/homeassistant/components/switch/light.py index 2650bd61bfb..e2154810522 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,5 +1,7 @@ """Light support for switch entities.""" -from typing import Any, Callable, Optional, Sequence, cast +from __future__ import annotations + +from typing import Any, Callable, Sequence, cast import voluptuous as vol @@ -12,15 +14,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - HomeAssistantType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -35,10 +33,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, async_add_entities: Callable[[Sequence[Entity]], None], - discovery_info: Optional[DiscoveryInfoType] = None, + discovery_info: DiscoveryInfoType | None = None, ) -> None: """Initialize Light Switch platform.""" @@ -65,7 +63,7 @@ class LightSwitch(LightEntity): self._name = name self._switch_entity_id = switch_entity_id self._unique_id = unique_id - self._switch_state: Optional[State] = None + self._switch_state: State | None = None @property def name(self) -> str: @@ -120,13 +118,11 @@ class LightSwitch(LightEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" - assert self.hass is not None self._switch_state = self.hass.states.get(self._switch_entity_id) @callback def async_state_changed_listener(*_: Any) -> None: """Handle child updates.""" - assert self.hass is not None self._switch_state = self.hass.states.get(self._switch_entity_id) self.async_write_ha_state() diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 0527f558f35..94fd836631b 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Switch state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -10,8 +12,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 @@ -21,11 +22,11 @@ VALID_STATES = {STATE_ON, STATE_OFF} async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -57,11 +58,11 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Switch states.""" await asyncio.gather( diff --git a/homeassistant/components/switch/significant_change.py b/homeassistant/components/switch/significant_change.py index f4dcddc3f34..231085a3eef 100644 --- a/homeassistant/components/switch/significant_change.py +++ b/homeassistant/components/switch/significant_change.py @@ -1,5 +1,7 @@ """Helper to test significant Switch state changes.""" -from typing import Any, Optional +from __future__ import annotations + +from typing import Any from homeassistant.core import HomeAssistant, callback @@ -12,6 +14,6 @@ def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs: Any, -) -> Optional[bool]: +) -> bool | None: """Test if state significantly changed.""" return old_state != new_state diff --git a/homeassistant/components/switch/translations/id.json b/homeassistant/components/switch/translations/id.json index 891b1b00681..070d272aa43 100644 --- a/homeassistant/components/switch/translations/id.json +++ b/homeassistant/components/switch/translations/id.json @@ -1,8 +1,23 @@ { + "device_automation": { + "action_type": { + "toggle": "Nyala/matikan {entity_name}", + "turn_off": "Matikan {entity_name}", + "turn_on": "Nyalakan {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} mati", + "is_on": "{entity_name} nyala" + }, + "trigger_type": { + "turned_off": "{entity_name} dimatikan", + "turned_on": "{entity_name} dinyalakan" + } + }, "state": { "_": { - "off": "Off", - "on": "On" + "off": "Mati", + "on": "Nyala" } }, "title": "Sakelar" diff --git a/homeassistant/components/switch/translations/ko.json b/homeassistant/components/switch/translations/ko.json index 1779f3e1f64..6a3417efeb6 100644 --- a/homeassistant/components/switch/translations/ko.json +++ b/homeassistant/components/switch/translations/ko.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "{entity_name} \ud1a0\uae00", - "turn_off": "{entity_name} \ub044\uae30", - "turn_on": "{entity_name} \ucf1c\uae30" + "toggle": "{entity_name}\uc744(\ub97c) \ud1a0\uae00\ud558\uae30", + "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", + "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + "is_off": "{entity_name}\uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name}\uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + "turned_off": "{entity_name}\uc774(\uac00) \uaebc\uc84c\uc744 \ub54c", + "turned_on": "{entity_name}\uc774(\uac00) \ucf1c\uc84c\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/switch/translations/zh-Hans.json b/homeassistant/components/switch/translations/zh-Hans.json index a18455aec6a..afe9f58db8f 100644 --- a/homeassistant/components/switch/translations/zh-Hans.json +++ b/homeassistant/components/switch/translations/zh-Hans.json @@ -7,7 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} \u5df2\u5173\u95ed", - "is_on": "{entity_name} \u5df2\u5f00\u542f" + "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { "turned_off": "{entity_name} \u88ab\u5173\u95ed", diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 3dd931abe49..cff1a0d0edc 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -1,7 +1,9 @@ """Support for Switchbot.""" -from typing import Any, Dict +from __future__ import annotations -# pylint: disable=import-error, no-member +from typing import Any + +# pylint: disable=import-error import switchbot import voluptuous as vol @@ -86,6 +88,6 @@ class SwitchBot(SwitchEntity, RestoreEntity): return self._name @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"last_run_success": self._last_run_success} diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index d081b3331c7..8d39182dcc3 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,8 +1,9 @@ """Home Assistant Switcher Component.""" +from __future__ import annotations + from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for from datetime import datetime, timedelta import logging -from typing import Dict, Optional from aioswitcher.bridge import SwitcherV2Bridge import voluptuous as vol @@ -45,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: +async def async_setup(hass: HomeAssistantType, config: dict) -> bool: """Set up the switcher component.""" phone_id = config[DOMAIN][CONF_PHONE_ID] device_id = config[DOMAIN][CONF_DEVICE_ID] @@ -72,7 +73,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config)) @callback - def device_updates(timestamp: Optional[datetime]) -> None: + def device_updates(timestamp: datetime | None) -> None: """Use for updating the device data from the queue.""" if v2bridge.running: try: diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 6b4b5026c2f..61297142716 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,5 +1,7 @@ """Home Assistant Switcher Component Switch platform.""" -from typing import Callable, Dict +from __future__ import annotations + +from typing import Callable from aioswitcher.api import SwitcherV2Api from aioswitcher.api.messages import SwitcherV2ControlResponseMSG @@ -52,9 +54,9 @@ SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { async def async_setup_platform( hass: HomeAssistantType, - config: Dict, + config: dict, async_add_entities: Callable, - discovery_info: Dict, + discovery_info: dict, ) -> None: """Set up the switcher platform for the switch component.""" if discovery_info is None: @@ -139,7 +141,7 @@ class SwitcherControl(SwitchEntity): return self._device_data.power_consumption @property - def device_state_attributes(self) -> Dict: + def extra_state_attributes(self) -> dict: """Return the optional state attributes.""" attribs = {} @@ -173,11 +175,11 @@ class SwitcherControl(SwitchEntity): self._state = self._device_data.state self.async_write_ha_state() - async def async_turn_on(self, **kwargs: Dict) -> None: + async def async_turn_on(self, **kwargs: dict) -> None: """Turn the entity on.""" await self._control_device(True) - async def async_turn_off(self, **kwargs: Dict) -> None: + async def async_turn_off(self, **kwargs: dict) -> None: """Turn the entity off.""" await self._control_device(False) diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index a052b9051a1..24b54537dc8 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -1,7 +1,7 @@ """Support for Switchmate.""" from datetime import timedelta -# pylint: disable=import-error, no-member +# pylint: disable=import-error import switchmate import voluptuous as vol diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 83d32eb9b47..293680151ff 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -1,7 +1,7 @@ """The syncthru component.""" +from __future__ import annotations import logging -from typing import Set, Tuple from pysyncthru import SyncThru @@ -65,12 +65,12 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return True -def device_identifiers(printer: SyncThru) -> Set[Tuple[str, str]]: +def device_identifiers(printer: SyncThru) -> set[tuple[str, str]]: """Get device identifiers for device registry.""" return {(DOMAIN, printer.serial_number())} -def device_connections(printer: SyncThru) -> Set[Tuple[str, str]]: +def device_connections(printer: SyncThru) -> set[tuple[str, str]]: """Get device connections for device registry.""" connections = set() try: diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index 83f044d8ebc..cb0243f98fc 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components import ssdp from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.helpers import aiohttp_client -# pylint: disable=unused-import # for DOMAIN https://github.com/PyCQA/pylint/issues/3202 from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN @@ -62,7 +61,6 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Remove trailing " (ip)" if present for consistency with user driven config self.name = re.sub(r"\s+\([\d.]+\)\s*$", "", self.name) - # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {CONF_NAME: self.name} return await self.async_step_confirm() diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 639ec3ac6cb..8277bd69467 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -5,11 +5,10 @@ import logging from pysyncthru import SYNCTHRU_STATE_HUMAN, SyncThru import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, PERCENTAGE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import device_identifiers from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN @@ -41,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the SyncThru component.""" _LOGGER.warning( "Loading syncthru via platform config is deprecated and no longer " - "necessary as of 0.113. Please remove it from your configuration YAML." + "necessary as of 0.113; Please remove it from your configuration YAML" ) hass.async_create_task( hass.config_entries.flow.async_init( @@ -81,7 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) -class SyncThruSensor(Entity): +class SyncThruSensor(SensorEntity): """Implementation of an abstract Samsung Printer sensor platform.""" def __init__(self, syncthru, name): @@ -121,7 +120,7 @@ class SyncThruSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._attributes diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json index 3b2d79a34a7..227b759ffaf 100644 --- a/homeassistant/components/syncthru/translations/hu.json +++ b/homeassistant/components/syncthru/translations/hu.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_url": "\u00c9rv\u00e9nytelen URL" + }, + "step": { + "confirm": { + "data": { + "name": "N\u00e9v", + "url": "Webes fel\u00fclet URL-je" + } + }, + "user": { + "data": { + "name": "N\u00e9v", + "url": "Webes fel\u00fclet URL-je" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/id.json b/homeassistant/components/syncthru/translations/id.json new file mode 100644 index 00000000000..54d5e6f5c96 --- /dev/null +++ b/homeassistant/components/syncthru/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_url": "URL tidak valid", + "syncthru_not_supported": "Perangkat tidak mendukung SyncThru", + "unknown_state": "Status printer tidak diketahui, verifikasi URL dan konektivitas jaringan" + }, + "flow_title": "Printer Samsung SyncThru: {name}", + "step": { + "confirm": { + "data": { + "name": "Nama", + "url": "URL antarmuka web" + } + }, + "user": { + "data": { + "name": "Nama", + "url": "URL antarmuka web" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/nl.json b/homeassistant/components/syncthru/translations/nl.json index 799e19ea371..d86e9fa2087 100644 --- a/homeassistant/components/syncthru/translations/nl.json +++ b/homeassistant/components/syncthru/translations/nl.json @@ -4,13 +4,16 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "invalid_url": "Ongeldige URL", + "syncthru_not_supported": "Apparaat ondersteunt SyncThru niet", "unknown_state": "Printerstatus onbekend, controleer URL en netwerkconnectiviteit" }, "flow_title": "Samsung SyncThru Printer: {name}", "step": { "confirm": { "data": { - "name": "Naam" + "name": "Naam", + "url": "Webinterface URL" } }, "user": { diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 6f0476b403c..16b531b9ee3 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,8 +1,9 @@ """The Synology DSM component.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Dict import async_timeout from synology_dsm import SynologyDSM @@ -71,7 +72,6 @@ from .const import ( STORAGE_VOL_SENSORS, SYNO_API, SYSTEM_LOADED, - TEMP_SENSORS_KEYS, UNDO_UPDATE_LISTENER, UTILISATION_SENSORS, ) @@ -191,7 +191,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await api.async_setup() except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.debug("async_setup_entry() - Unable to connect to DSM: %s", err) + _LOGGER.debug( + "Unable to connect to DSM '%s' during setup: %s", entry.unique_id, err + ) raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {}) @@ -225,9 +227,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async with async_timeout.timeout(10): await hass.async_add_executor_job(surveillance_station.update) except SynologyDSMAPIErrorException as err: - _LOGGER.debug( - "async_coordinator_update_data_cameras() - exception: %s", err - ) raise UpdateFailed(f"Error communicating with API: {err}") from err return { @@ -241,9 +240,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await api.async_update() except Exception as err: - _LOGGER.debug( - "async_coordinator_update_data_central() - exception: %s", err - ) raise UpdateFailed(f"Error communicating with API: {err}") from err return None @@ -338,15 +334,13 @@ async def _async_setup_services(hass: HomeAssistantType): serial = next(iter(dsm_devices)) else: _LOGGER.error( - "service_handler - more than one DSM configured, must specify one of serials %s", + "More than one DSM configured, must specify one of serials %s", sorted(dsm_devices), ) return if not dsm_device: - _LOGGER.error( - "service_handler - DSM with specified serial %s not found", serial - ) + _LOGGER.error("DSM with specified serial %s not found", serial) return _LOGGER.debug("%s DSM with serial %s", call.service, serial) @@ -409,11 +403,12 @@ class SynoApi: self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) ) _LOGGER.debug( - "SynoAPI.async_setup() - self._with_surveillance_station:%s", + "State of Surveillance_station during setup of '%s': %s", + self._entry.unique_id, self._with_surveillance_station, ) - self._setup_api_requests() + self._async_setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) await self.async_update() @@ -421,9 +416,7 @@ class SynoApi: @callback def subscribe(self, api_key, unique_id): """Subscribe an entity to API fetches.""" - _LOGGER.debug( - "SynoAPI.subscribe() - api_key:%s, unique_id:%s", api_key, unique_id - ) + _LOGGER.debug("Subscribe new entity: %s", unique_id) if api_key not in self._fetching_entities: self._fetching_entities[api_key] = set() self._fetching_entities[api_key].add(unique_id) @@ -431,9 +424,7 @@ class SynoApi: @callback def unsubscribe() -> None: """Unsubscribe an entity from API fetches (when disable).""" - _LOGGER.debug( - "SynoAPI.unsubscribe() - api_key:%s, unique_id:%s", api_key, unique_id - ) + _LOGGER.debug("Unsubscribe entity: %s", unique_id) self._fetching_entities[api_key].remove(unique_id) if len(self._fetching_entities[api_key]) == 0: self._fetching_entities.pop(api_key) @@ -441,21 +432,20 @@ class SynoApi: return unsubscribe @callback - def _setup_api_requests(self): + def _async_setup_api_requests(self): """Determine if we should fetch each API, if one entity needs it.""" # Entities not added yet, fetch all if not self._fetching_entities: _LOGGER.debug( - "SynoAPI._setup_api_requests() - Entities not added yet, fetch all" + "Entities not added yet, fetch all for '%s'", self._entry.unique_id ) return + # surveillance_station is updated by own coordinator + self.dsm.reset(self.surveillance_station) + # Determine if we should fetch an API self._with_system = bool(self.dsm.apis.get(SynoCoreSystem.API_KEY)) - self._with_surveillance_station = bool( - self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) - ) or bool(self.dsm.apis.get(SynoSurveillanceStation.HOME_MODE_API_KEY)) - self._with_security = bool( self._fetching_entities.get(SynoCoreSecurity.API_KEY) ) @@ -470,37 +460,42 @@ class SynoApi: # Reset not used API, information is not reset since it's used in device_info if not self._with_security: - _LOGGER.debug("SynoAPI._setup_api_requests() - disable security") + _LOGGER.debug( + "Disable security api from being updated for '%s'", + self._entry.unique_id, + ) self.dsm.reset(self.security) self.security = None if not self._with_storage: - _LOGGER.debug("SynoAPI._setup_api_requests() - disable storage") + _LOGGER.debug( + "Disable storage api from being updatedf or '%s'", self._entry.unique_id + ) self.dsm.reset(self.storage) self.storage = None if not self._with_system: - _LOGGER.debug("SynoAPI._setup_api_requests() - disable system") + _LOGGER.debug( + "Disable system api from being updated for '%s'", self._entry.unique_id + ) self.dsm.reset(self.system) self.system = None if not self._with_upgrade: - _LOGGER.debug("SynoAPI._setup_api_requests() - disable upgrade") + _LOGGER.debug( + "Disable upgrade api from being updated for '%s'", self._entry.unique_id + ) self.dsm.reset(self.upgrade) self.upgrade = None if not self._with_utilisation: - _LOGGER.debug("SynoAPI._setup_api_requests() - disable utilisation") + _LOGGER.debug( + "Disable utilisation api from being updated for '%s'", + self._entry.unique_id, + ) self.dsm.reset(self.utilisation) self.utilisation = None - if not self._with_surveillance_station: - _LOGGER.debug( - "SynoAPI._setup_api_requests() - disable surveillance_station" - ) - self.dsm.reset(self.surveillance_station) - self.surveillance_station = None - def _fetch_device_configuration(self): """Fetch initial device config.""" self.information = self.dsm.information @@ -508,28 +503,31 @@ class SynoApi: self.network.update() if self._with_security: - _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch security") + _LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id) self.security = self.dsm.security if self._with_storage: - _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch storage") + _LOGGER.debug("Enable storage api updates for '%s'", self._entry.unique_id) self.storage = self.dsm.storage if self._with_upgrade: - _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch upgrade") + _LOGGER.debug("Enable upgrade api updates for '%s'", self._entry.unique_id) self.upgrade = self.dsm.upgrade if self._with_system: - _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch system") + _LOGGER.debug("Enable system api updates for '%s'", self._entry.unique_id) self.system = self.dsm.system if self._with_utilisation: - _LOGGER.debug("SynoAPI._fetch_device_configuration() - fetch utilisation") + _LOGGER.debug( + "Enable utilisation api updates for '%s'", self._entry.unique_id + ) self.utilisation = self.dsm.utilisation if self._with_surveillance_station: _LOGGER.debug( - "SynoAPI._fetch_device_configuration() - fetch surveillance_station" + "Enable surveillance_station api updates for '%s'", + self._entry.unique_id, ) self.surveillance_station = self.dsm.surveillance_station @@ -538,7 +536,10 @@ class SynoApi: try: await self._hass.async_add_executor_job(self.system.reboot) except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.error("Reboot not possible, please try again later") + _LOGGER.error( + "Reboot of '%s' not possible, please try again later", + self._entry.unique_id, + ) _LOGGER.debug("Exception:%s", err) async def async_shutdown(self): @@ -546,7 +547,10 @@ class SynoApi: try: await self._hass.async_add_executor_job(self.system.shutdown) except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - _LOGGER.error("Shutdown not possible, please try again later") + _LOGGER.error( + "Shutdown of '%s' not possible, please try again later", + self._entry.unique_id, + ) _LOGGER.debug("Exception:%s", err) async def async_unload(self): @@ -554,21 +558,27 @@ class SynoApi: try: await self._hass.async_add_executor_job(self.dsm.logout) except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err: - _LOGGER.debug("Logout not possible:%s", err) + _LOGGER.debug( + "Logout from '%s' not possible:%s", self._entry.unique_id, err + ) async def async_update(self, now=None): """Update function for updating API information.""" - _LOGGER.debug("SynoAPI.async_update()") - self._setup_api_requests() + _LOGGER.debug("Start data update for '%s'", self._entry.unique_id) + self._async_setup_api_requests() try: await self._hass.async_add_executor_job( self.dsm.update, self._with_information ) except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: _LOGGER.warning( - "async_update - connection error during update, fallback by reloading the entry" + "Connection error during update, fallback by reloading the entry" + ) + _LOGGER.debug( + "Connection error during update of '%s' with exception: %s", + self._entry.unique_id, + err, ) - _LOGGER.debug("SynoAPI.async_update() - exception: %s", err) await self._hass.config_entries.async_reload(self._entry.entry_id) return @@ -580,7 +590,7 @@ class SynologyDSMBaseEntity(CoordinatorEntity): self, api: SynoApi, entity_type: str, - entity_info: Dict[str, str], + entity_info: dict[str, str], coordinator: DataUpdateCoordinator, ): """Initialize the Synology DSM entity.""" @@ -611,25 +621,18 @@ class SynologyDSMBaseEntity(CoordinatorEntity): """Return the icon.""" return self._icon - @property - def unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - if self.entity_type in TEMP_SENSORS_KEYS: - return self.hass.config.units.temperature_unit - return self._unit - @property def device_class(self) -> str: """Return the class of this device.""" return self._class @property - def device_state_attributes(self) -> Dict[str, any]: + def extra_state_attributes(self) -> dict[str, any]: """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> dict[str, any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial)}, @@ -657,7 +660,7 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): self, api: SynoApi, entity_type: str, - entity_info: Dict[str, str], + entity_info: dict[str, str], coordinator: DataUpdateCoordinator, device_id: str = None, ): @@ -699,7 +702,7 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): return bool(self._api.storage) @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> dict[str, any]: """Return the device information.""" return { "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 6e89f3d7a84..fb8ed5a23cd 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,5 +1,5 @@ """Support for Synology DSM binary sensors.""" -from typing import Dict +from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry @@ -71,7 +71,7 @@ class SynoDSMSecurityBinarySensor(SynologyDSMBaseEntity, BinarySensorEntity): return bool(self._api.security) @property - def device_state_attributes(self) -> Dict[str, str]: + def extra_state_attributes(self) -> dict[str, str]: """Return security checks details.""" return self._api.security.status_by_check diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index c0e0ded72ed..67052543569 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -1,6 +1,7 @@ """Support for Synology DSM cameras.""" +from __future__ import annotations + import logging -from typing import Dict from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( @@ -79,7 +80,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): return self.coordinator.data["cameras"][self._camera_id] @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> dict[str, any]: """Return the device information.""" return { "identifiers": { diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index e7b510bb399..b3b26e892a8 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -39,8 +39,8 @@ from .const import ( DEFAULT_TIMEOUT, DEFAULT_USE_SSL, DEFAULT_VERIFY_SSL, + DOMAIN, ) -from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 97f378c8e76..ba1aa393c85 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -128,21 +128,21 @@ UTILISATION_SENSORS = { ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": { - ENTITY_NAME: "CPU Load Averarge (1 min)", + ENTITY_NAME: "CPU Load Average (1 min)", ENTITY_UNIT: ENTITY_UNIT_LOAD, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": { - ENTITY_NAME: "CPU Load Averarge (5 min)", + ENTITY_NAME: "CPU Load Average (5 min)", ENTITY_UNIT: ENTITY_UNIT_LOAD, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": { - ENTITY_NAME: "CPU Load Averarge (15 min)", + ENTITY_NAME: "CPU Load Average (15 min)", ENTITY_UNIT: ENTITY_UNIT_LOAD, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 45cad8acfc2..4da44942b4f 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["synologydsm-api==1.0.1"], + "requirements": ["synologydsm-api==1.0.2"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 79350ce89d3..22f41601e7b 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,7 +1,9 @@ """Support for Synology DSM sensors.""" -from datetime import timedelta -from typing import Dict +from __future__ import annotations +from datetime import timedelta + +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DISKS, @@ -85,7 +87,18 @@ async def async_setup_entry( async_add_entities(entities) -class SynoDSMUtilSensor(SynologyDSMBaseEntity): +class SynoDSMSensor(SynologyDSMBaseEntity): + """Mixin for sensor specific attributes.""" + + @property + def unit_of_measurement(self) -> str: + """Return the unit the value is expressed in.""" + if self.entity_type in TEMP_SENSORS_KEYS: + return self.hass.config.units.temperature_unit + return self._unit + + +class SynoDSMUtilSensor(SynoDSMSensor, SensorEntity): """Representation a Synology Utilisation sensor.""" @property @@ -117,7 +130,7 @@ class SynoDSMUtilSensor(SynologyDSMBaseEntity): return bool(self._api.utilisation) -class SynoDSMStorageSensor(SynologyDSMDeviceEntity): +class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor, SensorEntity): """Representation a Synology Storage sensor.""" @property @@ -138,14 +151,14 @@ class SynoDSMStorageSensor(SynologyDSMDeviceEntity): return attr -class SynoDSMInfoSensor(SynologyDSMBaseEntity): +class SynoDSMInfoSensor(SynoDSMSensor, SensorEntity): """Representation a Synology information sensor.""" def __init__( self, api: SynoApi, entity_type: str, - entity_info: Dict[str, str], + entity_info: dict[str, str], coordinator: DataUpdateCoordinator, ): """Initialize the Synology SynoDSMInfoSensor entity.""" diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 998f74adf2a..f9883b0c916 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -1,6 +1,7 @@ """Support for Synology DSM switch.""" +from __future__ import annotations + import logging -from typing import Dict from synology_dsm.api.surveillance_station import SynoSurveillanceStation @@ -49,7 +50,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): self, api: SynoApi, entity_type: str, - entity_info: Dict[str, str], + entity_info: dict[str, str], version: str, coordinator: DataUpdateCoordinator, ): @@ -95,7 +96,7 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): return bool(self._api.surveillance_station) @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> dict[str, any]: """Return the device information.""" return { "identifiers": { diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json index 98b3a2214d7..8135fba13e0 100644 --- a/homeassistant/components/synology_dsm/translations/he.json +++ b/homeassistant/components/synology_dsm/translations/he.json @@ -1,4 +1,20 @@ { + "config": { + "step": { + "link": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index 29e520de432..c26fd349f06 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -1,23 +1,40 @@ { "config": { - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "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": "Synology DSM {name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "K\u00f3d" + } + }, "link": { "data": { "password": "Jelsz\u00f3", "port": "Port", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "title": "Synology DSM" }, "user": { "data": { "host": "Hoszt", "password": "Jelsz\u00f3", "port": "Port", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "title": "Synology DSM" } } } diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json new file mode 100644 index 00000000000..e614c2578d4 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "missing_data": "Data tidak tersedia: coba lagi nanti atau konfigurasikan lainnya", + "otp_failed": "Autentikasi dua langkah gagal, coba lagi dengan kode sandi baru", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "2sa": { + "data": { + "otp_code": "Kode" + }, + "title": "Synology DSM: autentikasi dua langkah" + }, + "link": { + "data": { + "password": "Kata Sandi", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Ingin menyiapkan {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "title": "Synology DSM" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pemindaian dalam menit", + "timeout": "Tenggang waktu (detik)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index efc20dbe03f..ab9dc4d445a 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -46,7 +46,7 @@ "step": { "init": { "data": { - "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ubd84)", + "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ubd84)", "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)" } } diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index d4932064a60..be8d7d45348 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Host is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -21,8 +21,8 @@ "link": { "data": { "password": "Wachtwoord", - "port": "Poort (optioneel)", - "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS", + "port": "Poort", + "ssl": "Gebruik een SSL-certificaat", "username": "Gebruikersnaam", "verify_ssl": "Controleer het SSL-certificaat" }, @@ -33,8 +33,8 @@ "data": { "host": "Host", "password": "Wachtwoord", - "port": "Poort (optioneel)", - "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS", + "port": "Poort", + "ssl": "Gebruik een SSL-certificaat", "username": "Gebruikersnaam", "verify_ssl": "Controleer het SSL-certificaat" }, diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 8c48b8c3fc7..98a7522b27d 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -23,7 +23,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", @@ -35,7 +35,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "title": "Synology DSM" diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index d5e78faf91c..19a231ccd60 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "missing_data": "\u7f3a\u5c11\u8cc7\u6599\uff1a\u8acb\u7a0d\u5f8c\u91cd\u8a66\u6216\u4f7f\u7528\u5176\u4ed6\u8a2d\u5b9a", - "otp_failed": "\u5169\u6b65\u9a5f\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u91cd\u65b0\u53d6\u5f97\u4ee3\u78bc\u5f8c\u91cd\u8a66", + "otp_failed": "\u96d9\u91cd\u8a8d\u8b49\u5931\u6557\uff0c\u8acb\u91cd\u65b0\u53d6\u5f97\u4ee3\u78bc\u5f8c\u91cd\u8a66", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "\u7fa4\u6689 DSM {name} ({host})", @@ -16,7 +16,7 @@ "data": { "otp_code": "\u4ee3\u78bc" }, - "title": "Synology DSM\uff1a\u96d9\u91cd\u9a57\u8b49" + "title": "Synology DSM\uff1a\u96d9\u91cd\u8a8d\u8b49" }, "link": { "data": { diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index c53cd9da1a5..2ad4863dbec 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -1,9 +1,11 @@ """Support for System health .""" +from __future__ import annotations + import asyncio import dataclasses from datetime import datetime import logging -from typing import Awaitable, Callable, Dict, Optional +from typing import Awaitable, Callable import aiohttp import async_timeout @@ -27,14 +29,14 @@ INFO_CALLBACK_TIMEOUT = 5 def async_register_info( hass: HomeAssistant, domain: str, - info_callback: Callable[[HomeAssistant], Dict], + info_callback: Callable[[HomeAssistant], dict], ): """Register an info callback. Deprecated. """ _LOGGER.warning( - "system_health.async_register_info is deprecated. Add a system_health platform instead." + "Calling system_health.async_register_info is deprecated; Add a system_health platform instead" ) hass.data.setdefault(DOMAIN, {}) SystemHealthRegistration(hass, domain).async_register_info(info_callback) @@ -58,7 +60,7 @@ async def _register_system_health_platform(hass, integration_domain, platform): async def get_integration_info( - hass: HomeAssistant, registration: "SystemHealthRegistration" + hass: HomeAssistant, registration: SystemHealthRegistration ): """Get integration system health.""" try: @@ -89,10 +91,10 @@ def _format_value(val): @websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "system_health/info"}) async def handle_info( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: Dict + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ): """Handle an info request via a subscription.""" - registrations: Dict[str, SystemHealthRegistration] = hass.data[DOMAIN] + registrations: dict[str, SystemHealthRegistration] = hass.data[DOMAIN] data = {} pending_info = {} @@ -187,14 +189,14 @@ class SystemHealthRegistration: hass: HomeAssistant domain: str - info_callback: Optional[Callable[[HomeAssistant], Awaitable[Dict]]] = None - manage_url: Optional[str] = None + info_callback: Callable[[HomeAssistant], Awaitable[dict]] | None = None + manage_url: str | None = None @callback def async_register_info( self, - info_callback: Callable[[HomeAssistant], Awaitable[Dict]], - manage_url: Optional[str] = None, + info_callback: Callable[[HomeAssistant], Awaitable[dict]], + manage_url: str | None = None, ): """Register an info callback.""" self.info_callback = info_callback @@ -203,7 +205,7 @@ class SystemHealthRegistration: async def async_check_can_reach_url( - hass: HomeAssistant, url: str, more_info: Optional[str] = None + hass: HomeAssistant, url: str, more_info: str | None = None ) -> str: """Test if the url can be reached.""" session = aiohttp_client.async_get_clientsession(hass) diff --git a/homeassistant/components/system_health/translations/id.json b/homeassistant/components/system_health/translations/id.json new file mode 100644 index 00000000000..309a6dd38d0 --- /dev/null +++ b/homeassistant/components/system_health/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Kesehatan Sistem" +} \ No newline at end of file diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index ce856c04c64..596f56d51a1 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -7,7 +7,7 @@ import sys import psutil import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_RESOURCES, CONF_TYPE, @@ -20,7 +20,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -165,24 +164,25 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Initialize the sensor argument if none was provided. # For disk monitoring default to "/" (root) to prevent runtime errors, if argument was not specified. if CONF_ARG not in resource: + resource[CONF_ARG] = "" if resource[CONF_TYPE].startswith("disk_"): resource[CONF_ARG] = "/" - else: - resource[CONF_ARG] = "" # Verify if we can retrieve CPU / processor temperatures. # If not, do not create the entity and add a warning to the log - if resource[CONF_TYPE] == "processor_temperature": - if SystemMonitorSensor.read_cpu_temperature() is None: - _LOGGER.warning("Cannot read CPU / processor temperature information.") - continue + if ( + resource[CONF_TYPE] == "processor_temperature" + and SystemMonitorSensor.read_cpu_temperature() is None + ): + _LOGGER.warning("Cannot read CPU / processor temperature information") + continue dev.append(SystemMonitorSensor(resource[CONF_TYPE], resource[CONF_ARG])) add_entities(dev, True) -class SystemMonitorSensor(Entity): +class SystemMonitorSensor(SensorEntity): """Implementation of a system monitor sensor.""" def __init__(self, sensor_type, argument=""): @@ -273,23 +273,20 @@ class SystemMonitorSensor(Entity): err.name, ) self._state = STATE_OFF - elif self.type == "network_out" or self.type == "network_in": + elif self.type in ["network_out", "network_in"]: counters = psutil.net_io_counters(pernic=True) if self.argument in counters: counter = counters[self.argument][IO_COUNTER[self.type]] self._state = round(counter / 1024 ** 2, 1) else: self._state = None - elif self.type == "packets_out" or self.type == "packets_in": + elif self.type in ["packets_out", "packets_in"]: counters = psutil.net_io_counters(pernic=True) if self.argument in counters: self._state = counters[self.argument][IO_COUNTER[self.type]] else: self._state = None - elif ( - self.type == "throughput_network_out" - or self.type == "throughput_network_in" - ): + elif self.type in ["throughput_network_out", "throughput_network_in"]: counters = psutil.net_io_counters(pernic=True) if self.argument in counters: counter = counters[self.argument][IO_COUNTER[self.type]] @@ -307,7 +304,7 @@ class SystemMonitorSensor(Entity): self._last_value = counter else: self._state = None - elif self.type == "ipv4_address" or self.type == "ipv6_address": + elif self.type in ["ipv4_address", "ipv6_address"]: addresses = psutil.net_if_addrs() if self.argument in addresses: for addr in addresses[self.argument]: @@ -334,16 +331,9 @@ class SystemMonitorSensor(Entity): temps = psutil.sensors_temperatures() for name, entries in temps.items(): - i = 1 - for entry in entries: + for i, entry in enumerate(entries, start=1): # In case the label is empty (e.g. on Raspberry PI 4), # construct it ourself here based on the sensor key name. - if not entry.label: - _label = f"{name} {i}" - else: - _label = entry.label - + _label = f"{name} {i}" if not entry.label else entry.label if _label in CPU_SENSOR_PREFIXES: return round(entry.current, 1) - - i += 1 diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index c7fb180e6d8..094465d38aa 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -31,7 +31,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TADO_COMPONENTS = ["binary_sensor", "sensor", "climate", "water_heater"] +PLATFORMS = ["binary_sensor", "sensor", "climate", "water_heater"] MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) SCAN_INTERVAL = timedelta(minutes=5) @@ -92,9 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): UPDATE_LISTENER: update_listener, } - for component in TADO_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -118,8 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in TADO_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -144,11 +144,13 @@ class TadoConnector: self._fallback = fallback self.home_id = None + self.home_name = None self.tado = None self.zones = None self.devices = None self.data = { "device": {}, + "weather": {}, "zone": {}, } @@ -164,7 +166,9 @@ class TadoConnector: # Load zones and devices self.zones = self.tado.getZones() self.devices = self.tado.getDevices() - self.home_id = self.tado.getMe()["homes"][0]["id"] + tado_home = self.tado.getMe()["homes"][0] + self.home_id = tado_home["id"] + self.home_name = tado_home["name"] @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -173,6 +177,11 @@ class TadoConnector: self.update_sensor("device", device["shortSerialNo"]) for zone in self.zones: self.update_sensor("zone", zone["id"]) + self.data["weather"] = self.tado.getWeather() + dispatcher_send( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "weather", "data"), + ) def update_sensor(self, sensor_type, sensor): """Update the internal data from Tado.""" diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 068c3a7ce93..9f68aa8a4e7 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -231,7 +231,7 @@ class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._state_attributes diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 9547617a36b..b86eb08b1b0 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -462,7 +462,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return temperature offset.""" return self._tado_zone_temp_offset diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 6c1f06b2626..5f97212abf3 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -9,8 +9,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import CONF_FALLBACK, UNIQUE_ID -from .const import DOMAIN # pylint:disable=unused-import +from .const import CONF_FALLBACK, DOMAIN, UNIQUE_ID _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 6e009df7ca2..2c86fa2d642 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -48,6 +48,21 @@ CONF_FALLBACK = "fallback" DATA = "data" UPDATE_TRACK = "update_track" +# Weather +CONDITIONS_MAP = { + "clear-night": {"NIGHT_CLEAR"}, + "cloudy": {"CLOUDY", "CLOUDY_MOSTLY", "NIGHT_CLOUDY"}, + "fog": {"FOGGY"}, + "hail": {"HAIL", "RAIN_HAIL"}, + "lightning": {"THUNDERSTORM"}, + "partlycloudy": {"CLOUDY_PARTLY"}, + "rainy": {"DRIZZLE", "RAIN", "SCATTERED_RAIN"}, + "snowy": {"FREEZING", "SCATTERED_SNOW", "SNOW"}, + "snowy-rainy": {"RAIN_SNOW", "SCATTERED_RAIN_SNOW"}, + "sunny": {"SUN"}, + "windy": {"WIND"}, +} + # Types TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" TYPE_HEATING = "HEATING" @@ -149,6 +164,7 @@ UNIQUE_ID = "unique_id" DEFAULT_NAME = "Tado" +TADO_HOME = "Home" TADO_ZONE = "Zone" UPDATE_LISTENER = "update_listener" diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 8de938af985..afa8bc6a604 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -131,11 +131,10 @@ class TadoDeviceScanner(DeviceScanner): # Find devices that have geofencing enabled, and are currently at home. for mobile_device in tado_json: - if mobile_device.get("location"): - if mobile_device["location"]["atHome"]: - device_id = mobile_device["id"] - device_name = mobile_device["name"] - last_results.append(Device(device_id, device_name)) + if mobile_device.get("location") and mobile_device["location"]["atHome"]: + device_id = mobile_device["id"] + device_name = mobile_device["name"] + last_results.append(Device(device_id, device_name)) self.last_results = last_results diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 34473a45c98..270d6f1e911 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -1,7 +1,7 @@ """Base class for Tado entity.""" from homeassistant.helpers.entity import Entity -from .const import DEFAULT_NAME, DOMAIN, TADO_ZONE +from .const import DEFAULT_NAME, DOMAIN, TADO_HOME, TADO_ZONE class TadoDeviceEntity(Entity): @@ -32,6 +32,26 @@ class TadoDeviceEntity(Entity): return False +class TadoHomeEntity(Entity): + """Base implementation for Tado home.""" + + def __init__(self, tado): + """Initialize a Tado home.""" + super().__init__() + self.home_name = tado.home_name + self.home_id = tado.home_id + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.home_id)}, + "name": self.home_name, + "manufacturer": DEFAULT_NAME, + "model": TADO_HOME, + } + + class TadoZoneEntity(Entity): """Base implementation for Tado zone.""" diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 6613de82bff..87d2170eb75 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,6 +1,7 @@ """Support for Tado sensors for each zone.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, @@ -10,9 +11,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import ( + CONDITIONS_MAP, DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, @@ -20,10 +21,16 @@ from .const import ( TYPE_HEATING, TYPE_HOT_WATER, ) -from .entity import TadoZoneEntity +from .entity import TadoHomeEntity, TadoZoneEntity _LOGGER = logging.getLogger(__name__) +HOME_SENSORS = { + "outdoor temperature", + "solar percentage", + "weather condition", +} + ZONE_SENSORS = { TYPE_HEATING: [ "temperature", @@ -41,6 +48,14 @@ ZONE_SENSORS = { } +def format_condition(condition: str) -> str: + """Return condition from dict CONDITIONS_MAP.""" + for key, value in CONDITIONS_MAP.items(): + if condition in value: + return key + return condition + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): @@ -50,6 +65,9 @@ async def async_setup_entry( zones = tado.zones entities = [] + # Create home sensors + entities.extend([TadoHomeSensor(tado, variable) for variable in HOME_SENSORS]) + # Create zone sensors for zone in zones: zone_type = zone["type"] @@ -68,7 +86,112 @@ async def async_setup_entry( async_add_entities(entities, True) -class TadoZoneSensor(TadoZoneEntity, Entity): +class TadoHomeSensor(TadoHomeEntity, SensorEntity): + """Representation of a Tado Sensor.""" + + def __init__(self, tado, home_variable): + """Initialize of the Tado Sensor.""" + super().__init__(tado) + self._tado = tado + + self.home_variable = home_variable + + self._unique_id = f"{home_variable} {tado.home_id}" + + self._state = None + self._state_attributes = None + self._tado_weather_data = self._tado.data["weather"] + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format( + self._tado.home_id, "weather", "data" + ), + self._async_update_callback, + ) + ) + self._async_update_home_data() + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._tado.home_name} {self.home_variable}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._state_attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self.home_variable == "temperature": + return TEMP_CELSIUS + if self.home_variable == "solar percentage": + return PERCENTAGE + if self.home_variable == "weather condition": + return None + + @property + def device_class(self): + """Return the device class.""" + if self.home_variable == "outdoor temperature": + return DEVICE_CLASS_TEMPERATURE + return None + + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_home_data() + self.async_write_ha_state() + + @callback + def _async_update_home_data(self): + """Handle update callbacks.""" + try: + self._tado_weather_data = self._tado.data["weather"] + except KeyError: + return + + if self.home_variable == "outdoor temperature": + self._state = self.hass.config.units.temperature( + self._tado_weather_data["outsideTemperature"]["celsius"], + TEMP_CELSIUS, + ) + self._state_attributes = { + "time": self._tado_weather_data["outsideTemperature"]["timestamp"], + } + + elif self.home_variable == "solar percentage": + self._state = self._tado_weather_data["solarIntensity"]["percentage"] + self._state_attributes = { + "time": self._tado_weather_data["solarIntensity"]["timestamp"], + } + + elif self.home_variable == "weather condition": + self._state = format_condition( + self._tado_weather_data["weatherState"]["value"] + ) + self._state_attributes = { + "time": self._tado_weather_data["weatherState"]["timestamp"] + } + + +class TadoZoneSensor(TadoZoneEntity, SensorEntity): """Representation of a tado Sensor.""" def __init__(self, tado, zone_name, zone_id, zone_variable): @@ -114,7 +237,7 @@ class TadoZoneSensor(TadoZoneEntity, Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._state_attributes diff --git a/homeassistant/components/tado/translations/he.json b/homeassistant/components/tado/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/tado/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/hu.json b/homeassistant/components/tado/translations/hu.json index dee4ed9ee0f..fd8db27da5e 100644 --- a/homeassistant/components/tado/translations/hu.json +++ b/homeassistant/components/tado/translations/hu.json @@ -1,5 +1,13 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tado/translations/id.json b/homeassistant/components/tado/translations/id.json new file mode 100644 index 00000000000..bdbfa19cb6d --- /dev/null +++ b/homeassistant/components/tado/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "no_homes": "Tidak ada rumah yang ditautkan ke akun Tado ini.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Hubungkan ke akun Tado Anda" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Aktifkan mode alternatif." + }, + "title": "Sesuaikan opsi Tado." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/ko.json b/homeassistant/components/tado/translations/ko.json index 8290e32c4c8..29603d8f6d2 100644 --- a/homeassistant/components/tado/translations/ko.json +++ b/homeassistant/components/tado/translations/ko.json @@ -25,7 +25,7 @@ "data": { "fallback": "\ub300\uccb4 \ubaa8\ub4dc\ub97c \ud65c\uc131\ud654\ud569\ub2c8\ub2e4." }, - "description": "\uc601\uc5ed\uc744 \uc218\ub3d9\uc73c\ub85c \uc804\ud658\ud558\uba74 \ub300\uccb4 \ubaa8\ub4dc\ub294 \ub2e4\uc74c \uc77c\uc815\uc744 \uc2a4\ub9c8\ud2b8 \uc77c\uc815\uc73c\ub85c \uc804\ud658\ud569\ub2c8\ub2e4.", + "description": "\ub300\uccb4 \ubaa8\ub4dc\ub294 \uc9c0\uc5ed\uc744 \uc218\ub3d9\uc73c\ub85c \uc870\uc815\ud55c \ud6c4 \ub2e4\uc74c \uc77c\uc815 \uc804\ud658\uc2dc \uc2a4\ub9c8\ud2b8 \uc77c\uc815\uc73c\ub85c \uc804\ud658\ub429\ub2c8\ub2e4.", "title": "Tado \uc635\uc158 \uc870\uc815\ud558\uae30" } } diff --git a/homeassistant/components/tado/translations/nl.json b/homeassistant/components/tado/translations/nl.json index 3cdadf0f54e..3b6d914b71c 100644 --- a/homeassistant/components/tado/translations/nl.json +++ b/homeassistant/components/tado/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "no_homes": "Er zijn geen huizen gekoppeld aan dit tado-account.", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/tado/translations/ru.json b/homeassistant/components/tado/translations/ru.json index 75c83e8582b..18e9ddff67b 100644 --- a/homeassistant/components/tado/translations/ru.json +++ b/homeassistant/components/tado/translations/ru.json @@ -13,7 +13,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Tado" } diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 6c385181aaa..6dcf2ec9a4d 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -1,6 +1,7 @@ """The Tag integration.""" +from __future__ import annotations + import logging -import typing import uuid import voluptuous as vol @@ -63,7 +64,7 @@ class TagStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" data = self.CREATE_SCHEMA(data) if not data[TAG_ID]: @@ -74,11 +75,11 @@ class TagStorageCollection(collection.StorageCollection): return data @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[TAG_ID] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" data = {**data, **self.UPDATE_SCHEMA(update_data)} # make last_scanned JSON serializeable diff --git a/homeassistant/components/tag/translations/hu.json b/homeassistant/components/tag/translations/hu.json new file mode 100644 index 00000000000..edea7ba32ef --- /dev/null +++ b/homeassistant/components/tag/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "C\u00edmke" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/id.json b/homeassistant/components/tag/translations/id.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/nl.json b/homeassistant/components/tag/translations/nl.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index 9803bd56afe..4f6dd89a252 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -18,6 +18,7 @@ TRIGGER_SCHEMA = vol.Schema( async def async_attach_trigger(hass, config, action, automation_info): """Listen for tag_scanned events based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None tag_ids = set(config[TAG_ID]) device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None @@ -37,6 +38,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "platform": DOMAIN, "event": event, "description": "Tag scanned", + "id": trigger_id, } }, event.context, diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index d75ccaec414..8db7b23a8ce 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -TAHOMA_COMPONENTS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"] TAHOMA_TYPES = { "io:AwningValanceIOComponent": "cover", @@ -73,7 +73,7 @@ TAHOMA_TYPES = { def setup(hass, config): - """Activate Tahoma component.""" + """Set up Tahoma integration.""" conf = config[DOMAIN] username = conf.get(CONF_USERNAME) @@ -111,14 +111,14 @@ def setup(hass, config): for scene in scenes: hass.data[DOMAIN]["scenes"].append(scene) - for component in TAHOMA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) return True def map_tahoma_device(tahoma_device): - """Map Tahoma device types to Home Assistant components.""" + """Map Tahoma device types to Home Assistant platforms.""" return TAHOMA_TYPES.get(tahoma_device.type) @@ -137,7 +137,7 @@ class TahomaDevice(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return {"tahoma_device_id": self.tahoma_device.url} diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index af06bf5ca4c..c0946013469 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -57,10 +57,10 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorEntity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().device_state_attributes + super_attr = super().extra_state_attributes if super_attr is not None: attr.update(super_attr) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index 2eec9160811..a02f21fb5e1 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -194,10 +194,10 @@ class TahomaCover(TahomaDevice, CoverEntity): return TAHOMA_DEVICE_CLASSES.get(self.tahoma_device.type) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().device_state_attributes + super_attr = super().extra_state_attributes if super_attr is not None: attr.update(super_attr) diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py index 93d82bffc99..3d160cd95b3 100644 --- a/homeassistant/components/tahoma/lock.py +++ b/homeassistant/components/tahoma/lock.py @@ -78,12 +78,12 @@ class TahomaLock(TahomaDevice, LockEntity): return self._lock_status == STATE_LOCKED @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the lock state attributes.""" attr = { ATTR_BATTERY_LEVEL: self._battery_level, } - super_attr = super().device_state_attributes + super_attr = super().extra_state_attributes if super_attr is not None: attr.update(super_attr) return attr diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py index 1d53b65d5d5..3cfa26a5f89 100644 --- a/homeassistant/components/tahoma/scene.py +++ b/homeassistant/components/tahoma/scene.py @@ -37,6 +37,6 @@ class TahomaScene(Scene): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the scene.""" return {"tahoma_scene_oid": self.tahoma_scene.oid} diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index fb1129cfa0e..47e6d300414 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -2,8 +2,8 @@ from datetime import timedelta import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_BATTERY_LEVEL, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS -from homeassistant.helpers.entity import Entity from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices, True) -class TahomaSensor(TahomaDevice, Entity): +class TahomaSensor(TahomaDevice, SensorEntity): """Representation of a Tahoma Sensor.""" def __init__(self, tahoma_device, controller): @@ -110,10 +110,10 @@ class TahomaSensor(TahomaDevice, Entity): _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().device_state_attributes + super_attr = super().extra_state_attributes if super_attr is not None: attr.update(super_attr) diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index 808f80d8cfa..2ea68b93e6b 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -105,10 +105,10 @@ class TahomaSwitch(TahomaDevice, SwitchEntity): return bool(self._state == STATE_ON) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().device_state_attributes + super_attr = super().extra_state_attributes if super_attr is not None: attr.update(super_attr) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index ab1cc8b23da..379819cf65e 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -7,10 +7,9 @@ import requests from tank_utility import auth, device as tank_monitor import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, PERCENTAGE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -62,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(all_sensors, True) -class TankUtilitySensor(Entity): +class TankUtilitySensor(SensorEntity): """Representation of a Tank Utility sensor.""" def __init__(self, email, password, token, device): @@ -97,7 +96,7 @@ class TankUtilitySensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the device.""" return self._attributes diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 6985072b065..5c1898e02a9 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -2,6 +2,7 @@ import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -79,7 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class FuelPriceSensor(CoordinatorEntity): +class FuelPriceSensor(CoordinatorEntity, SensorEntity): """Contains prices for fuel in a given station.""" def __init__(self, fuel_type, station, coordinator, name, show_on_map): @@ -125,7 +126,7 @@ class FuelPriceSensor(CoordinatorEntity): return f"{self._station_id}_{self._fuel_type}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the device.""" data = self.coordinator.data[self._station_id] diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index c0ebae7695e..83baae9c19c 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -24,7 +24,6 @@ from homeassistant.helpers.device_registry import ( EVENT_DEVICE_REGISTRY_UPDATED, async_entries_for_config_entry, ) -from homeassistant.helpers.typing import HomeAssistantType from . import device_automation, discovery from .const import ( @@ -38,11 +37,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: dict): - """Set up the Tasmota component.""" - return True - - async def async_setup_entry(hass, entry): """Set up Tasmota from a config entry.""" websocket_api.async_register_command(hass, websocket_remove_device) @@ -92,8 +86,8 @@ async def async_setup_entry(hass, entry): await device_automation.async_setup_entry(hass, entry) await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS ] ) @@ -113,8 +107,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -128,8 +122,8 @@ async def async_unload_entry(hass, entry): for unsub in hass.data[DATA_UNSUB]: unsub() hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation"))() - for component in PLATFORMS: - hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(component))() + for platform in PLATFORMS: + hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(platform))() # deattach device triggers device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index 5d39fa02438..320b4ff2448 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -4,11 +4,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.mqtt import valid_subscribe_topic -from .const import ( # pylint:disable=unused-import - CONF_DISCOVERY_PREFIX, - DEFAULT_PREFIX, - DOMAIN, -) +from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 463b1c65a98..ae4a528efc6 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -1,6 +1,8 @@ """Provides device automations for Tasmota.""" +from __future__ import annotations + import logging -from typing import Callable, List, Optional +from typing import Callable import attr from hatasmota.trigger import TasmotaTrigger @@ -46,8 +48,8 @@ class TriggerInstance: action: AutomationActionType = attr.ib() automation_info: dict = attr.ib() - trigger: "Trigger" = attr.ib() - remove: Optional[CALLBACK_TYPE] = attr.ib(default=None) + trigger: Trigger = attr.ib() + remove: CALLBACK_TYPE | None = attr.ib(default=None) async def async_attach_trigger(self): """Attach event trigger.""" @@ -85,7 +87,7 @@ class Trigger: subtype: str = attr.ib() tasmota_trigger: TasmotaTrigger = attr.ib() type: str = attr.ib() - trigger_instances: List[TriggerInstance] = attr.ib(factory=list) + trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger(self, action, automation_info): """Add Tasmota trigger.""" @@ -238,7 +240,7 @@ async def async_remove_triggers(hass: HomeAssistant, device_id: str): device_trigger.remove_update_signal() -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for a Tasmota device.""" triggers = [] @@ -271,7 +273,6 @@ async def async_attach_trigger( """Attach a device trigger.""" if DEVICE_TRIGGERS not in hass.data: hass.data[DEVICE_TRIGGERS] = {} - config = TRIGGER_SCHEMA(config) device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 77b4532c001..876d1a4cf60 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -1,7 +1,5 @@ """Support for Tasmota fans.""" -from typing import Optional - from hatasmota import const as tasmota_const from homeassistant.components import fan @@ -59,7 +57,7 @@ class TasmotaFan( ) @property - def speed_count(self) -> Optional[int]: + def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return len(ORDERED_NAMED_FAN_SPEEDS) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 17a6e2a35c2..432fc2266f3 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -1,14 +1,16 @@ """Support for Tasmota sensors.""" -from typing import Optional +from __future__ import annotations from hatasmota import const as hc, status_sensor from homeassistant.components import sensor +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -37,7 +39,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW @@ -52,7 +53,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER}, hc.SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY}, hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"}, - hc.SENSOR_CO2: {ICON: "mdi:molecule-co2"}, + hc.SENSOR_CO2: {DEVICE_CLASS: DEVICE_CLASS_CO2}, hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"}, hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"}, @@ -144,7 +145,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity): +class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): """Representation of a Tasmota sensor.""" def __init__(self, **kwds): @@ -162,7 +163,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity): self.async_write_ha_state() @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" class_or_icon = SENSOR_DEVICE_CLASS_ICON_MAP.get( self._tasmota_entity.quantity, {} @@ -192,6 +193,11 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity): return self._state.isoformat() return self._state + @property + def force_update(self): + """Force update.""" + return True + @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json index c76efd0e898..4461f2a2b71 100644 --- a/homeassistant/components/tasmota/translations/hu.json +++ b/homeassistant/components/tasmota/translations/hu.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "step": { "config": { + "description": "Add meg a Tasmota konfigur\u00e1ci\u00f3t.", "title": "Tasmota" + }, + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Tasmota-t?" } } } diff --git a/homeassistant/components/tasmota/translations/id.json b/homeassistant/components/tasmota/translations/id.json new file mode 100644 index 00000000000..a11acf50390 --- /dev/null +++ b/homeassistant/components/tasmota/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_discovery_topic": "Prefiks topik penemuan tidak valid." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "Prefiks topik penemuan" + }, + "description": "Masukkan konfigurasi Tasmota.", + "title": "Tasmota" + }, + "confirm": { + "description": "Ingin menyiapkan Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/ko.json b/homeassistant/components/tasmota/translations/ko.json index c6e52d209e7..45cac13f622 100644 --- a/homeassistant/components/tasmota/translations/ko.json +++ b/homeassistant/components/tasmota/translations/ko.json @@ -1,7 +1,22 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." + }, + "error": { + "invalid_discovery_topic": "\uac80\uc0c9 \ud1a0\ud53d \uc811\ub450\uc0ac\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "config": { + "data": { + "discovery_prefix": "\uac80\uc0c9 \ud1a0\ud53d \uc811\ub450\uc0ac" + }, + "description": "Tasmota \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Tasmota" + }, + "confirm": { + "description": "Tasmota\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/nl.json b/homeassistant/components/tasmota/translations/nl.json index 3b0cc5c1ca8..c099d376920 100644 --- a/homeassistant/components/tasmota/translations/nl.json +++ b/homeassistant/components/tasmota/translations/nl.json @@ -3,8 +3,14 @@ "abort": { "single_instance_allowed": "Is al geconfigureerd. Er is maar een configuratie mogelijk" }, + "error": { + "invalid_discovery_topic": "Ongeldig onderwerpvoorvoegsel voor ontdekken" + }, "step": { "config": { + "data": { + "discovery_prefix": "Discovery-onderwerpvoorvoegsel" + }, "description": "Vul de Tasmota gegevens in", "title": "Tasmota" }, diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index ed96bb62ace..c50efb00ed7 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from pytautulli import Tautulli import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -18,7 +18,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle CONF_MONITORED_USERS = "monitored_users" @@ -72,7 +71,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensor, True) -class TautulliSensor(Entity): +class TautulliSensor(SensorEntity): """Representation of a Tautulli sensor.""" def __init__(self, tautulli, name, monitored_conditions, users): @@ -130,7 +129,7 @@ class TautulliSensor(Entity): return "Watching" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return attributes for the sensor.""" return self._attributes diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 9b7e1539fb4..54cf4d120f1 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -5,7 +5,7 @@ import socket import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -48,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([TcpSensor(hass, config)]) -class TcpSensor(Entity): +class TcpSensor(SensorEntity): """Implementation of a TCP socket based sensor.""" required = () diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 3ea26286b18..d618ca9c2cf 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -1,4 +1,5 @@ """Support gathering ted5000 information.""" +from contextlib import suppress from datetime import timedelta import logging @@ -6,10 +7,9 @@ import requests import voluptuous as vol import xmltodict -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT, VOLT from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class Ted5000Sensor(Entity): +class Ted5000Sensor(SensorEntity): """Implementation of a Ted5000 sensor.""" def __init__(self, gateway, name, mtu, unit): @@ -74,10 +74,8 @@ class Ted5000Sensor(Entity): @property def state(self): """Return the state of the resources.""" - try: + with suppress(KeyError): return self._gateway.data[self._mtu][self._unit] - except KeyError: - pass def update(self): """Get the latest data from REST API.""" diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index b6ca7881615..86bf4c24407 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -330,7 +330,7 @@ async def async_setup(hass, config): attribute_templ = data.get(attribute) if attribute_templ: if any( - [isinstance(attribute_templ, vtype) for vtype in [float, int, str]] + isinstance(attribute_templ, vtype) for vtype in [float, int, str] ): data[attribute] = attribute_templ else: diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index f772e2411e5..7fd6cb24efd 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -42,7 +42,7 @@ async def async_setup_platform(hass, config): if (last_error_date is not None) and (isinstance(last_error_date, int)): last_error_date = dt.datetime.fromtimestamp(last_error_date) _LOGGER.info( - "telegram webhook last_error_date: %s. Status: %s", + "Telegram webhook last_error_date: %s. Status: %s", last_error_date, current_status, ) diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 5d4721e60e6..70cc8848814 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -123,8 +123,8 @@ async def async_unload_entry(hass, config_entry): interval_tracker() await asyncio.wait( [ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in hass.data.pop(CONFIG_ENTRY_IS_SETUP) + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in hass.data.pop(CONFIG_ENTRY_IS_SETUP) ] ) del hass.data[DOMAIN] diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index aabbf88ee1c..33a02cd1f16 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -29,7 +29,7 @@ KEY_TOKEN_SECRET = "token_secret" _LOGGER = logging.getLogger(__name__) -@config_entries.HANDLERS.register("tellduslive") +@config_entries.HANDLERS.register(DOMAIN) class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index 851823385dc..4453622b21e 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -81,7 +81,7 @@ class TelldusLiveEntity(Entity): return self._client.is_available(self.device_id) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {} if self._battery_level: diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 1b06fd6ed97..a86b487afd2 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -1,5 +1,6 @@ """Support for Tellstick Net/Telstick Live sensors.""" from homeassistant.components import sensor, tellduslive +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -71,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TelldusLiveSensor(TelldusLiveEntity): +class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): """Representation of a Telldus Live sensor.""" @property diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json index 748189e6427..2e59375369d 100644 --- a/homeassistant/components/tellduslive/translations/hu.json +++ b/homeassistant/components/tellduslive/translations/hu.json @@ -1,16 +1,20 @@ { "config": { "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "user": { "data": { "host": "Hoszt" }, - "description": "\u00dcres", "title": "V\u00e1lassz v\u00e9gpontot." } } diff --git a/homeassistant/components/tellduslive/translations/id.json b/homeassistant/components/tellduslive/translations/id.json new file mode 100644 index 00000000000..1a405e794fe --- /dev/null +++ b/homeassistant/components/tellduslive/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi." + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "auth": { + "description": "Untuk menautkan akun TelldusLive Anda:\n 1. Klik tautan di bawah ini\n 2. Masuk ke Telldus Live\n 3. Otorisasi **{app_name}** (klik **Yes**).\n 4. Kembali ke sini dan klik **KIRIM**.\n\n[Akun Link TelldusLive] ({auth_url})", + "title": "Autentikasi ke TelldusLive" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Pilih titik akhir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json index d29dd504844..6107245c088 100644 --- a/homeassistant/components/tellduslive/translations/ko.json +++ b/homeassistant/components/tellduslive/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "unknown_authorize_url_generation": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." @@ -12,7 +12,7 @@ }, "step": { "auth": { - "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live \uc5d0 \ub85c\uadf8\uc778 \ud558\uc138\uc694\n 3. Authorize **{app_name}** (**Yes** \ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **\ud655\uc778**\uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})", + "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live\uc5d0 \ub85c\uadf8\uc778\ud574\uc8fc\uc138\uc694\n 3. **{app_name}**\uc744(\ub97c) \uc778\uc99d\ud574\uc8fc\uc138\uc694 (**Yes**\ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **\ud655\uc778**\uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})", "title": "TelldusLive \uc778\uc99d\ud558\uae30" }, "user": { diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json index 4eb6d40a142..c34911553ab 100644 --- a/homeassistant/components/tellduslive/translations/nl.json +++ b/homeassistant/components/tellduslive/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Service is al geconfigureerd", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "unknown": "Onbekende fout opgetreden", + "unknown": "Onverwachte fout", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "error": { diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 444cabd0180..f58c5916bfb 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -6,7 +6,7 @@ from tellcore import telldus import tellcore.constants as tellcore_constants import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_ID, CONF_NAME, @@ -15,7 +15,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -126,7 +125,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class TellstickSensor(Entity): +class TellstickSensor(SensorEntity): """Representation of a Tellstick sensor.""" def __init__(self, name, tellcore_sensor, datatype, sensor_info): diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index c47aa1878fc..7edbd3ba812 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -4,14 +4,13 @@ import logging from temperusb.temper import TemperHandler import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_NAME, CONF_OFFSET, DEVICE_DEFAULT_NAME, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -58,7 +57,7 @@ def reset_devices(): sensor.set_temper_device(device) -class TemperSensor(Entity): +class TemperSensor(SensorEntity): """Representation of a Temper temperature sensor.""" def __init__(self, temper_device, temp_unit, name, scaling): diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index cc8862afcf4..72a97d6eeab 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1,20 +1,143 @@ """The template component.""" -from homeassistant.const import SERVICE_RELOAD +from __future__ import annotations + +import asyncio +import logging +from typing import Callable + +from homeassistant import config as conf_util +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_START, SERVICE_RELOAD +from homeassistant.core import CoreState, Event, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import ( + discovery, + trigger as trigger_helper, + update_coordinator, +) from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.loader import async_get_integration -from .const import DOMAIN, EVENT_TEMPLATE_RELOADED, PLATFORMS +from .const import CONF_TRIGGER, DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) -async def async_setup_reload_service(hass): - """Create the reload service for the template domain.""" - if hass.services.has_service(DOMAIN, SERVICE_RELOAD): - return +async def async_setup(hass, config): + """Set up the template integration.""" + if DOMAIN in config: + await _process_config(hass, config) + + async def _reload_config(call: Event) -> None: + """Reload top-level + platforms.""" + try: + unprocessed_conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + conf = await conf_util.async_process_component_config( + hass, unprocessed_conf, await async_get_integration(hass, DOMAIN) + ) + + if conf is None: + return - async def _reload_config(call): - """Reload the template platform config.""" await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) - hass.bus.async_fire(EVENT_TEMPLATE_RELOADED, context=call.context) + + if DOMAIN in conf: + await _process_config(hass, conf) + + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) hass.helpers.service.async_register_admin_service( DOMAIN, SERVICE_RELOAD, _reload_config ) + + return True + + +async def _process_config(hass, config): + """Process config.""" + coordinators: list[TriggerUpdateCoordinator] | None = hass.data.get(DOMAIN) + + # Remove old ones + if coordinators: + for coordinator in coordinators: + coordinator.async_remove() + + async def init_coordinator(hass, conf): + coordinator = TriggerUpdateCoordinator(hass, conf) + await coordinator.async_setup(conf) + return coordinator + + hass.data[DOMAIN] = await asyncio.gather( + *[init_coordinator(hass, conf) for conf in config[DOMAIN]] + ) + + +class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): + """Class to handle incoming data.""" + + REMOVE_TRIGGER = object() + + def __init__(self, hass, config): + """Instantiate trigger data.""" + super().__init__(hass, _LOGGER, name="Trigger Update Coordinator") + self.config = config + self._unsub_start: Callable[[], None] | None = None + self._unsub_trigger: Callable[[], None] | None = None + + @property + def unique_id(self) -> str | None: + """Return unique ID for the entity.""" + return self.config.get("unique_id") + + @callback + def async_remove(self): + """Signal that the entities need to remove themselves.""" + if self._unsub_start: + self._unsub_start() + if self._unsub_trigger: + self._unsub_trigger() + + async def async_setup(self, hass_config): + """Set up the trigger and create entities.""" + if self.hass.state == CoreState.running: + await self._attach_triggers() + else: + self._unsub_start = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._attach_triggers + ) + + for platform_domain in (SENSOR_DOMAIN,): + self.hass.async_create_task( + discovery.async_load_platform( + self.hass, + platform_domain, + DOMAIN, + {"coordinator": self, "entities": self.config[platform_domain]}, + hass_config, + ) + ) + + async def _attach_triggers(self, start_event=None) -> None: + """Attach the triggers.""" + if start_event is not None: + self._unsub_start = None + + self._unsub_trigger = await trigger_helper.async_initialize_triggers( + self.hass, + self.config[CONF_TRIGGER], + self._handle_triggered, + DOMAIN, + self.name, + self.logger.log, + start_event is not None, + ) + + @callback + def _handle_triggered(self, run_variables, context=None): + self.async_set_updated_data( + {"run_variables": run_variables, "context": context} + ) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index f56c5b27572..4c72c5094ef 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -32,10 +32,8 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from .const import DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -113,7 +111,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template Alarm Control Panels.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index b810c7faee1..1088652cd0a 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -22,10 +22,9 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import result_as_boolean -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity CONF_DELAY_ON = "delay_on" @@ -97,7 +96,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py new file mode 100644 index 00000000000..5d1a66836f3 --- /dev/null +++ b/homeassistant/components/template/config.py @@ -0,0 +1,128 @@ +"""Template config validator.""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON, + CONF_ICON_TEMPLATE, + CONF_NAME, + CONF_SENSORS, + CONF_STATE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.trigger import async_validate_trigger_config + +from .const import ( + CONF_ATTRIBUTE_TEMPLATES, + CONF_ATTRIBUTES, + CONF_AVAILABILITY, + CONF_AVAILABILITY_TEMPLATE, + CONF_PICTURE, + CONF_TRIGGER, + DOMAIN, +) +from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA + +LEGACY_SENSOR = { + CONF_ICON_TEMPLATE: CONF_ICON, + CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE, + CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY, + CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES, + CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME, + CONF_FRIENDLY_NAME: CONF_NAME, + CONF_VALUE_TEMPLATE: CONF_STATE, +} + + +SENSOR_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + +CONFIG_SECTION_SCHEMA = vol.Schema( + { + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, + vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA), + } +) + + +def _rewrite_legacy_to_modern_trigger_conf(cfg: dict): + """Rewrite a legacy to a modern trigger-basd conf.""" + logging.getLogger(__name__).warning( + "The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors" + ) + sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else [] + + for device_id, entity_cfg in cfg[CONF_SENSORS].items(): + entity_cfg = {**entity_cfg} + + for from_key, to_key in LEGACY_SENSOR.items(): + if from_key not in entity_cfg or to_key in entity_cfg: + continue + + val = entity_cfg.pop(from_key) + if isinstance(val, str): + val = template.Template(val) + entity_cfg[to_key] = val + + if CONF_NAME not in entity_cfg: + entity_cfg[CONF_NAME] = template.Template(device_id) + + sensor.append(entity_cfg) + + return {**cfg, "sensor": sensor} + + +async def async_validate_config(hass, config): + """Validate config.""" + if DOMAIN not in config: + return config + + config_sections = [] + + for cfg in cv.ensure_list(config[DOMAIN]): + try: + cfg = CONFIG_SECTION_SCHEMA(cfg) + cfg[CONF_TRIGGER] = await async_validate_trigger_config( + hass, cfg[CONF_TRIGGER] + ) + except vol.Invalid as err: + async_log_exception(err, DOMAIN, cfg, hass) + continue + + if CONF_TRIGGER in cfg and CONF_SENSORS in cfg: + cfg = _rewrite_legacy_to_modern_trigger_conf(cfg) + + config_sections.append(cfg) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = config_sections + + return config diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 5b38f19eaeb..971d4a864c9 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -1,13 +1,13 @@ """Constants for the Template Platform Components.""" CONF_AVAILABILITY_TEMPLATE = "availability_template" +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" +CONF_TRIGGER = "trigger" DOMAIN = "template" PLATFORM_STORAGE_KEY = "template_platforms" -EVENT_TEMPLATE_RELOADED = "event_template_reloaded" - PLATFORMS = [ "alarm_control_panel", "binary_sensor", @@ -20,3 +20,7 @@ PLATFORMS = [ "vacuum", "weather", ] + +CONF_AVAILABILITY = "availability" +CONF_ATTRIBUTES = "attributes" +CONF_PICTURE = "picture" diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 278cd1c80bb..cd552a33e5d 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -38,10 +38,9 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -160,7 +159,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template cover.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 51dce0f8d56..563a9af2849 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -36,10 +36,9 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -163,7 +162,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template fans.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) @@ -274,6 +272,11 @@ class TemplateFan(TemplateEntity, FanEntity): # List of valid preset modes self._preset_modes = preset_modes + @property + def _implemented_speed(self): + """Return true if speed has been implemented.""" + return bool(self._set_speed_script or self._speed_template) + @property def name(self): """Return the display name of this fan.""" @@ -292,7 +295,7 @@ class TemplateFan(TemplateEntity, FanEntity): @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return self._speed_count or super().speed_count + return self._speed_count or 100 @property def speed_list(self) -> list: diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 0edaacbb5ca..e76ba42289b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -31,10 +31,9 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -137,7 +136,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lights.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 692f06e28fe..c4a3977a4db 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -13,10 +13,9 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity CONF_LOCK = "lock" @@ -60,7 +59,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lock.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index c67e3a275a3..4631a775847 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,35 +1,36 @@ """Allows the creation of a sensor that breaks out state_attributes.""" -from typing import Optional +from __future__ import annotations import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA, + DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + SensorEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICE_CLASS, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME_TEMPLATE, CONF_ICON_TEMPLATE, CONF_SENSORS, + CONF_STATE, CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity, async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import async_generate_entity_id -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE, CONF_TRIGGER from .template_entity import TemplateEntity - -CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" +from .trigger_entity import TriggerEntity SENSOR_SCHEMA = vol.All( cv.deprecated(ATTR_ENTITY_ID), @@ -43,8 +44,8 @@ SENSOR_SCHEMA = vol.All( vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( {cv.string: cv.template} ), - vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, @@ -52,12 +53,31 @@ SENSOR_SCHEMA = vol.All( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)} + +def trigger_warning(val): + """Warn if a trigger is defined.""" + if CONF_TRIGGER in val: + raise vol.Invalid( + "You can only add triggers to template entities if they are defined under `template:`. " + "See the template documentation for more information: https://www.home-assistant.io/integrations/template/" + ) + + return val + + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_TRIGGER): cv.match_all, # to raise custom warning + vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), + } + ), + trigger_warning, ) -async def _async_create_entities(hass, config): +@callback +def _async_create_template_tracking_entities(hass, config): """Create the template sensors.""" sensors = [] @@ -66,11 +86,11 @@ async def _async_create_entities(hass, config): icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) - unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) + unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT) device_class = device_config.get(CONF_DEVICE_CLASS) - attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) unique_id = device_config.get(CONF_UNIQUE_ID) sensors.append( @@ -95,11 +115,16 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - async_add_entities(await _async_create_entities(hass, config)) + if discovery_info is None: + async_add_entities(_async_create_template_tracking_entities(hass, config)) + else: + async_add_entities( + TriggerSensorEntity(hass, discovery_info["coordinator"], config) + for config in discovery_info["entities"] + ) -class SensorTemplate(TemplateEntity, Entity): +class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" def __init__( @@ -165,7 +190,7 @@ class SensorTemplate(TemplateEntity, Entity): return self._state @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class @@ -173,3 +198,15 @@ class SensorTemplate(TemplateEntity, Entity): def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + + +class TriggerSensorEntity(TriggerEntity, SensorEntity): + """Sensor entity based on trigger data.""" + + domain = SENSOR_DOMAIN + extra_template_keys = (CONF_STATE,) + + @property + def state(self) -> str | None: + """Return state of the sensor.""" + return self._rendered.get(CONF_STATE) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 412c4507d1f..0e083df13f4 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -22,11 +22,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -90,7 +89,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template switches.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index f350eb87d61..f8909206dec 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -1,7 +1,8 @@ """TemplateEntity utility class.""" +from __future__ import annotations import logging -from typing import Any, Callable, List, Optional, Union +from typing import Any, Callable import voluptuous as vol @@ -30,8 +31,8 @@ class _TemplateAttribute: attribute: str, template: Template, validator: Callable[[Any], Any] = None, - on_update: Optional[Callable[[Any], None]] = None, - none_on_template_error: Optional[bool] = False, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool | None = False, ): """Template attribute.""" self._entity = entity @@ -61,10 +62,10 @@ class _TemplateAttribute: @callback def handle_result( self, - event: Optional[Event], + event: Event | None, template: Template, - last_result: Union[str, None, TemplateError], - result: Union[str, TemplateError], + last_result: str | None | TemplateError, + result: str | TemplateError, ) -> None: """Handle a template result event callback.""" if isinstance(result, TemplateError): @@ -168,7 +169,7 @@ class TemplateEntity(Entity): return self._entity_picture @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes @@ -189,7 +190,7 @@ class TemplateEntity(Entity): attribute: str, template: Template, validator: Callable[[Any], Any] = None, - on_update: Optional[Callable[[Any], None]] = None, + on_update: Callable[[Any], None] | None = None, none_on_template_error: bool = False, ) -> None: """ @@ -219,8 +220,8 @@ class TemplateEntity(Entity): @callback def _handle_results( self, - event: Optional[Event], - updates: List[TrackTemplateResult], + event: Event | None, + updates: list[TrackTemplateResult], ) -> None: """Call back the results to the attributes.""" if event: diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 1f378c59335..e631950a74a 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -31,6 +31,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="template" ): """Listen for state changes based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass time_delta = config.get(CONF_FOR) @@ -100,6 +101,7 @@ async def async_attach_trigger( trigger_variables = { "for": time_delta, "description": description, + "id": trigger_id, } @callback diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py new file mode 100644 index 00000000000..418fa976304 --- /dev/null +++ b/homeassistant/components/template/trigger_entity.py @@ -0,0 +1,145 @@ +"""Trigger entity.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import template, update_coordinator + +from . import TriggerUpdateCoordinator +from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE + + +class TriggerEntity(update_coordinator.CoordinatorEntity): + """Template entity based on trigger data.""" + + domain = "" + extra_template_keys: tuple | None = None + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: dict, + ): + """Initialize the entity.""" + super().__init__(coordinator) + + entity_unique_id = config.get(CONF_UNIQUE_ID) + + if entity_unique_id and coordinator.unique_id: + self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}" + else: + self._unique_id = entity_unique_id + + self._config = config + + self._static_rendered = {} + self._to_render = [] + + for itm in ( + CONF_NAME, + CONF_ICON, + CONF_PICTURE, + CONF_AVAILABILITY, + ): + if itm not in config: + continue + + if config[itm].is_static: + self._static_rendered[itm] = config[itm].template + else: + self._to_render.append(itm) + + if self.extra_template_keys is not None: + self._to_render.extend(self.extra_template_keys) + + # We make a copy so our initial render is 'unknown' and not 'unavailable' + self._rendered = dict(self._static_rendered) + + @property + def name(self): + """Name of the entity.""" + return self._rendered.get(CONF_NAME) + + @property + def unique_id(self): + """Return unique ID of the entity.""" + return self._unique_id + + @property + def device_class(self): + """Return device class of the entity.""" + return self._config.get(CONF_DEVICE_CLASS) + + @property + def unit_of_measurement(self) -> str | None: + """Return unit of measurement.""" + return self._config.get(CONF_UNIT_OF_MEASUREMENT) + + @property + def icon(self) -> str | None: + """Return icon.""" + return self._rendered.get(CONF_ICON) + + @property + def entity_picture(self) -> str | None: + """Return entity picture.""" + return self._rendered.get(CONF_PICTURE) + + @property + def available(self): + """Return availability of the entity.""" + return ( + self._rendered is not self._static_rendered + and + # Check against False so `None` is ok + self._rendered.get(CONF_AVAILABILITY) is not False + ) + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return extra attributes.""" + return self._rendered.get(CONF_ATTRIBUTES) + + async def async_added_to_hass(self) -> None: + """Handle being added to Home Assistant.""" + template.attach(self.hass, self._config) + await super().async_added_to_hass() + if self.coordinator.data is not None: + self._handle_coordinator_update() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + try: + rendered = dict(self._static_rendered) + + for key in self._to_render: + rendered[key] = self._config[key].async_render( + self.coordinator.data["run_variables"], parse_result=False + ) + + if CONF_ATTRIBUTES in self._config: + rendered[CONF_ATTRIBUTES] = template.render_complex( + self._config[CONF_ATTRIBUTES], + self.coordinator.data["run_variables"], + ) + + self._rendered = rendered + except template.TemplateError as err: + logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( + "Error rendering %s template for %s: %s", key, self.entity_id, err + ) + self._rendered = self._static_rendered + + self.async_set_context(self.coordinator.data["context"]) + self.async_write_ha_state() diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 171aeb7af92..ed7919d174e 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -41,10 +41,9 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS +from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -147,7 +146,6 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template vacuums.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 560bd5639ba..eabafb89803 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -2,6 +2,7 @@ import voluptuous as vol from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, ATTR_CONDITION_FOG, @@ -23,12 +24,11 @@ from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.reload import async_setup_reload_service -from .const import DOMAIN, PLATFORMS from .template_entity import TemplateEntity CONDITION_CLASSES = { + ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, ATTR_CONDITION_FOG, ATTR_CONDITION_HAIL, @@ -49,8 +49,12 @@ CONF_WEATHER = "weather" CONF_TEMPERATURE_TEMPLATE = "temperature_template" CONF_HUMIDITY_TEMPLATE = "humidity_template" CONF_CONDITION_TEMPLATE = "condition_template" +CONF_ATTRIBUTION_TEMPLATE = "attribution_template" CONF_PRESSURE_TEMPLATE = "pressure_template" CONF_WIND_SPEED_TEMPLATE = "wind_speed_template" +CONF_WIND_BEARING_TEMPLATE = "wind_bearing_template" +CONF_OZONE_TEMPLATE = "ozone_template" +CONF_VISIBILITY_TEMPLATE = "visibility_template" CONF_FORECAST_TEMPLATE = "forecast_template" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -59,8 +63,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_CONDITION_TEMPLATE): cv.template, vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template, vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template, vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, + vol.Optional(CONF_OZONE_TEMPLATE): cv.template, + vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, } @@ -69,14 +77,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template weather.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) name = config[CONF_NAME] condition_template = config[CONF_CONDITION_TEMPLATE] temperature_template = config[CONF_TEMPERATURE_TEMPLATE] humidity_template = config[CONF_HUMIDITY_TEMPLATE] + attribution_template = config.get(CONF_ATTRIBUTION_TEMPLATE) pressure_template = config.get(CONF_PRESSURE_TEMPLATE) wind_speed_template = config.get(CONF_WIND_SPEED_TEMPLATE) + wind_bearing_template = config.get(CONF_WIND_BEARING_TEMPLATE) + ozone_template = config.get(CONF_OZONE_TEMPLATE) + visibility_template = config.get(CONF_VISIBILITY_TEMPLATE) forecast_template = config.get(CONF_FORECAST_TEMPLATE) unique_id = config.get(CONF_UNIQUE_ID) @@ -88,8 +99,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= condition_template, temperature_template, humidity_template, + attribution_template, pressure_template, wind_speed_template, + wind_bearing_template, + ozone_template, + visibility_template, forecast_template, unique_id, ) @@ -107,8 +122,12 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): condition_template, temperature_template, humidity_template, + attribution_template, pressure_template, wind_speed_template, + wind_bearing_template, + ozone_template, + visibility_template, forecast_template, unique_id, ): @@ -119,8 +138,12 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._condition_template = condition_template self._temperature_template = temperature_template self._humidity_template = humidity_template + self._attribution_template = attribution_template self._pressure_template = pressure_template self._wind_speed_template = wind_speed_template + self._wind_bearing_template = wind_bearing_template + self._ozone_template = ozone_template + self._visibility_template = visibility_template self._forecast_template = forecast_template self._unique_id = unique_id @@ -129,8 +152,12 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._condition = None self._temperature = None self._humidity = None + self._attribution = None self._pressure = None self._wind_speed = None + self._wind_bearing = None + self._ozone = None + self._visibility = None self._forecast = [] @property @@ -163,9 +190,24 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): """Return the wind speed.""" return self._wind_speed + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._wind_bearing + + @property + def ozone(self): + """Return the ozone level.""" + return self._ozone + + @property + def visibility(self): + """Return the visibility.""" + return self._visibility + @property def pressure(self): - """Return the pressure.""" + """Return the air pressure.""" return self._pressure @property @@ -176,11 +218,13 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): @property def attribution(self): """Return the attribution.""" - return "Powered by Home Assistant" + if self._attribution is None: + return "Powered by Home Assistant" + return self._attribution @property def unique_id(self): - """Return the unique id of this light.""" + """Return the unique id of this weather instance.""" return self._unique_id async def async_added_to_hass(self): @@ -202,6 +246,11 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): "_humidity", self._humidity_template, ) + if self._attribution_template: + self.add_template_attribute( + "_attribution", + self._attribution_template, + ) if self._pressure_template: self.add_template_attribute( "_pressure", @@ -212,6 +261,21 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): "_wind_speed", self._wind_speed_template, ) + if self._wind_bearing_template: + self.add_template_attribute( + "_wind_bearing", + self._wind_bearing_template, + ) + if self._ozone_template: + self.add_template_attribute( + "_ozone", + self._ozone_template, + ) + if self._visibility_template: + self.add_template_attribute( + "_visibility", + self._visibility_template, + ) if self._forecast_template: self.add_template_attribute( "_forecast", diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index e387ae97afe..dad83005512 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -279,7 +279,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): return self._total_matches @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return { ATTR_MATCHES: self._matches, @@ -336,14 +336,13 @@ class TensorFlowImageProcessor(ImageProcessingEntity): """Process the image.""" model = self.hass.data[DOMAIN][CONF_MODEL] if not model: - _LOGGER.debug("Model not yet ready.") + _LOGGER.debug("Model not yet ready") return start = time.perf_counter() try: - import cv2 # pylint: disable=import-error, import-outside-toplevel + import cv2 # pylint: disable=import-outside-toplevel - # pylint: disable=no-member img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) inp = img[:, :, [2, 1, 0]] # BGR->RGB inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 300c3ddd1db..84619680490 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,8 +6,8 @@ "tensorflow==2.3.0", "tf-models-official==2.3.0", "pycocotools==2.0.1", - "numpy==1.19.2", - "pillow==8.1.1" + "numpy==1.20.2", + "pillow==8.1.2" ], "codeowners": [] } diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index b31f8ae6dd3..5091d2ea102 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -9,7 +9,7 @@ from teslajsonpy import Controller as TeslaAPI from teslajsonpy.exceptions import IncompleteCredentials, TeslaException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -21,7 +21,6 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -39,7 +38,7 @@ from .const import ( DOMAIN, ICONS, MIN_SCAN_INTERVAL, - TESLA_COMPONENTS, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) @@ -134,7 +133,8 @@ async def async_setup_entry(hass, config_entry): """Set up Tesla as config entry.""" hass.data.setdefault(DOMAIN, {}) config = config_entry.data - websession = aiohttp_client.async_get_clientsession(hass) + # Because users can have multiple accounts, we always create a new session so they have separate cookies + websession = aiohttp_client.async_create_clientsession(hass) email = config_entry.title if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]: scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL] @@ -178,9 +178,7 @@ async def async_setup_entry(hass, config_entry): } _LOGGER.debug("Connected to the Tesla API") - await coordinator.async_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() all_devices = controller.get_homeassistant_components() @@ -190,10 +188,10 @@ async def async_setup_entry(hass, config_entry): for device in all_devices: entry_data["devices"][device.hass_type].append(device) - for component in TESLA_COMPONENTS: - _LOGGER.debug("Loading %s", component) + for platform in PLATFORMS: + _LOGGER.debug("Loading %s", platform) hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -203,8 +201,8 @@ async def async_unload_entry(hass, config_entry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in TESLA_COMPONENTS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -222,7 +220,7 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": "reauth"}, + context={"source": SOURCE_REAUTH}, data=entry.data, ) ) @@ -307,7 +305,7 @@ class TeslaDevice(CoordinatorEntity): return ICONS.get(self.tesla_device.type) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" attr = self._attributes if self.tesla_device.has_battery(): diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 4c7ed850749..81639bc3fe4 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -1,6 +1,7 @@ """Support for Tesla HVAC system.""" +from __future__ import annotations + import logging -from typing import List, Optional from teslajsonpy.exceptions import UnknownPresetMode @@ -103,7 +104,7 @@ class TeslaThermostat(TeslaDevice, ClimateEntity): _LOGGER.error("%s", ex.message) @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp. Requires SUPPORT_PRESET_MODE. @@ -111,7 +112,7 @@ class TeslaThermostat(TeslaDevice, ClimateEntity): return self.tesla_device.preset_mode @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes. Requires SUPPORT_PRESET_MODE. diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 8dce5e238ac..7b3060c5072 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -20,9 +20,9 @@ from .const import ( CONF_WAKE_ON_START, DEFAULT_SCAN_INTERVAL, DEFAULT_WAKE_ON_START, + DOMAIN, MIN_SCAN_INTERVAL, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -147,7 +147,8 @@ async def validate_input(hass: core.HomeAssistant, data): """ config = {} - websession = aiohttp_client.async_get_clientsession(hass) + websession = aiohttp_client.async_create_clientsession(hass) + try: controller = TeslaAPI( websession, diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 2b8485c7616..94883e4a833 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -5,7 +5,8 @@ DATA_LISTENER = "listener" DEFAULT_SCAN_INTERVAL = 660 DEFAULT_WAKE_ON_START = False MIN_SCAN_INTERVAL = 60 -TESLA_COMPONENTS = [ + +PLATFORMS = [ "sensor", "lock", "climate", @@ -13,6 +14,7 @@ TESLA_COMPONENTS = [ "device_tracker", "switch", ] + ICONS = { "battery sensor": "mdi:battery", "range sensor": "mdi:gauge", diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index cac89d58d3a..6813b3769e7 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,5 +1,5 @@ """Support for tracking Tesla cars.""" -from typing import Optional +from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity @@ -25,13 +25,13 @@ class TeslaDeviceEntity(TeslaDevice, TrackerEntity): """A class representing a Tesla device.""" @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of the device.""" location = self.tesla_device.get_location() return self.tesla_device.get_location().get("latitude") if location else None @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of the device.""" location = self.tesla_device.get_location() return self.tesla_device.get_location().get("longitude") if location else None @@ -42,9 +42,9 @@ class TeslaDeviceEntity(TeslaDevice, TrackerEntity): return SOURCE_TYPE_GPS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" - attr = super().device_state_attributes.copy() + attr = super().extra_state_attributes.copy() location = self.tesla_device.get_location() if location: attr.update( diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 3b66845c786..40c7aa8548d 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -1,14 +1,13 @@ """Support for the Tesla sensors.""" -from typing import Optional +from __future__ import annotations -from homeassistant.components.sensor import DEVICE_CLASSES +from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_MILES, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity from homeassistant.util.distance import convert from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -27,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class TeslaSensor(TeslaDevice, Entity): +class TeslaSensor(TeslaDevice, SensorEntity): """Representation of Tesla sensors.""" def __init__(self, tesla_device, coordinator, sensor_type=None): @@ -39,7 +38,7 @@ class TeslaSensor(TeslaDevice, Entity): self._unique_id = f"{super().unique_id}_{self.type}" @property - def state(self) -> Optional[float]: + def state(self) -> float | None: """Return the state of the sensor.""" if self.tesla_device.type == "temperature sensor": if self.type == "outside": @@ -58,7 +57,7 @@ class TeslaSensor(TeslaDevice, Entity): return self.tesla_device.get_value() @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit_of_measurement of the device.""" units = self.tesla_device.measurement if units == "F": @@ -72,7 +71,7 @@ class TeslaSensor(TeslaDevice, Entity): return units @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device_class of the device.""" return ( self.tesla_device.device_class @@ -81,7 +80,7 @@ class TeslaSensor(TeslaDevice, Entity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" attr = self._attributes.copy() if self.tesla_device.type == "charging rate sensor": diff --git a/homeassistant/components/tesla/translations/he.json b/homeassistant/components/tesla/translations/he.json new file mode 100644 index 00000000000..ac90b3264ea --- /dev/null +++ b/homeassistant/components/tesla/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/hu.json b/homeassistant/components/tesla/translations/hu.json index c6553d4a595..a4622ce7efa 100644 --- a/homeassistant/components/tesla/translations/hu.json +++ b/homeassistant/components/tesla/translations/hu.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tesla/translations/id.json b/homeassistant/components/tesla/translations/id.json new file mode 100644 index 00000000000..681504d0d42 --- /dev/null +++ b/homeassistant/components/tesla/translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "description": "Masukkan informasi Anda.", + "title": "Tesla - Konfigurasi" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "Paksa mobil bangun saat dinyalakan", + "scan_interval": "Interval pemindaian dalam detik" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/ko.json b/homeassistant/components/tesla/translations/ko.json index 3e3893e0b75..285326f39de 100644 --- a/homeassistant/components/tesla/translations/ko.json +++ b/homeassistant/components/tesla/translations/ko.json @@ -25,7 +25,7 @@ "init": { "data": { "enable_wake_on_start": "\uc2dc\ub3d9 \uc2dc \ucc28\ub7c9 \uae68\uc6b0\uae30", - "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ucd08)" + "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" } } } diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json index f6289de6d9d..5655a641f96 100644 --- a/homeassistant/components/tesla/translations/nl.json +++ b/homeassistant/components/tesla/translations/nl.json @@ -13,7 +13,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "E-mailadres" + "username": "E-mail" }, "description": "Vul alstublieft uw gegevens in.", "title": "Tesla - Configuratie" diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 83a2fd12d24..86427349f31 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -11,7 +11,7 @@ from stringcase import camelcase, snakecase import thermoworks_smoke import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_EMAIL, @@ -21,7 +21,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -91,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error(msg) -class ThermoworksSmokeSensor(Entity): +class ThermoworksSmokeSensor(SensorEntity): """Implementation of a thermoworks smoke sensor.""" def __init__(self, sensor_type, serial, mgr): @@ -124,7 +123,7 @@ class ThermoworksSmokeSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 2e7b7f9499b..2e139eae63d 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -7,7 +7,7 @@ from aiohttp.hdrs import ACCEPT, AUTHORIZATION import async_timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_TIME, @@ -18,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DATA_TTN, TTN_ACCESS_KEY, TTN_APP_ID, TTN_DATA_STORAGE_URL @@ -59,7 +58,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices, True) -class TtnDataSensor(Entity): +class TtnDataSensor(SensorEntity): """Representation of a The Things Network Data Storage sensor.""" def __init__(self, ttn_data_storage, device_id, value, unit_of_measurement): @@ -81,8 +80,8 @@ class TtnDataSensor(Entity): """Return the state of the entity.""" if self._ttn_data_storage.data is not None: try: - return round(self._state[self._value], 1) - except (KeyError, TypeError): + return self._state[self._value] + except KeyError: return None return None @@ -92,7 +91,7 @@ class TtnDataSensor(Entity): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" if self._ttn_data_storage.data is not None: return { diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 966930f1eb1..56cf272f7d1 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -5,10 +5,9 @@ from pythinkingcleaner import Discovery, ThinkingCleaner import voluptuous as vol from homeassistant import util -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, PERCENTAGE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -73,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev) -class ThinkingCleanerSensor(Entity): +class ThinkingCleanerSensor(SensorEntity): """Representation of a ThinkingCleaner Sensor.""" def __init__(self, tc_object, sensor_type, update_devices): diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index fa05fc09687..5bd6f77253b 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -144,7 +144,7 @@ class ThresholdSensor(BinarySensorEntity): return TYPE_UPPER @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_ENTITY_ID: self._entity_id, diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index b4b29a84297..fd7fc389c75 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -73,9 +73,9 @@ async def async_setup_entry(hass, entry): _LOGGER.error("Failed to login. %s", exp) return False - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) # set up notify platform, no entry support for notify component yet, @@ -93,8 +93,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index 8dce1e66a5a..2c498cc2ff5 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 652804859da..108f05d5625 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.16.1"], + "requirements": ["pyTibber==0.16.2"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index bb5ebe8011f..5ab85013a25 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -6,10 +6,9 @@ from random import randrange import aiohttp -from homeassistant.components.sensor import DEVICE_CLASS_POWER +from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity from homeassistant.const import POWER_WATT from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER @@ -45,7 +44,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(dev, True) -class TibberSensor(Entity): +class TibberSensor(SensorEntity): """Representation of a generic Tibber sensor.""" def __init__(self, tibber_home): @@ -54,7 +53,7 @@ class TibberSensor(Entity): self._last_updated = None self._state = None self._is_available = False - self._device_state_attributes = {} + self._extra_state_attributes = {} self._name = tibber_home.info["viewer"]["home"]["appNickname"] if self._name is None: self._name = tibber_home.info["viewer"]["home"]["address"].get( @@ -63,9 +62,9 @@ class TibberSensor(Entity): self._spread_load_constant = randrange(3600) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - return self._device_state_attributes + return self._extra_state_attributes @property def model(self): @@ -121,10 +120,10 @@ class TibberSensorElPrice(TibberSensor): res = self._tibber_home.current_price_data() self._state, price_level, self._last_updated = res - self._device_state_attributes["price_level"] = price_level + self._extra_state_attributes["price_level"] = price_level attrs = self._tibber_home.current_attributes() - self._device_state_attributes.update(attrs) + self._extra_state_attributes.update(attrs) self._is_available = self._state is not None @property @@ -165,11 +164,11 @@ class TibberSensorElPrice(TibberSensor): except (asyncio.TimeoutError, aiohttp.ClientError): return data = self._tibber_home.info["viewer"]["home"] - self._device_state_attributes["app_nickname"] = data["appNickname"] - self._device_state_attributes["grid_company"] = data["meteringPointData"][ + self._extra_state_attributes["app_nickname"] = data["appNickname"] + self._extra_state_attributes["grid_company"] = data["meteringPointData"][ "gridCompany" ] - self._device_state_attributes["estimated_annual_consumption"] = data[ + self._extra_state_attributes["estimated_annual_consumption"] = data[ "meteringPointData" ]["estimatedAnnualConsumption"] @@ -197,7 +196,7 @@ class TibberSensorRT(TibberSensor): for key, value in live_measurement.items(): if value is None: continue - self._device_state_attributes[key] = value + self._extra_state_attributes[key] = value self.async_write_ha_state() diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json index 08a622fd238..6ad59022845 100644 --- a/homeassistant/components/tibber/translations/hu.json +++ b/homeassistant/components/tibber/translations/hu.json @@ -1,14 +1,20 @@ { "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a Tibberhez val\u00f3 csatlakoz\u00e1skor" }, "step": { "user": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" - } + }, + "description": "Add meg a hozz\u00e1f\u00e9r\u00e9si tokent a https://developer.tibber.com/settings/accesstoken c\u00edmr\u0151l", + "title": "Tibber" } } } diff --git a/homeassistant/components/tibber/translations/id.json b/homeassistant/components/tibber/translations/id.json new file mode 100644 index 00000000000..479cf83f8c7 --- /dev/null +++ b/homeassistant/components/tibber/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_access_token": "Token akses tidak valid", + "timeout": "Tenggang waktu terhubung ke Tibber habis" + }, + "step": { + "user": { + "data": { + "access_token": "Token Akses" + }, + "description": "Masukkan token akses Anda dari https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 205742017d3..48bf8177c63 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -74,9 +74,9 @@ async def async_setup_entry(hass, entry): await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -87,8 +87,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 87f58193e9d..7cc98e7a77f 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index f7cc4e1736e..7571e235ef1 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -81,7 +81,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" return self._attrs diff --git a/homeassistant/components/tile/translations/hu.json b/homeassistant/components/tile/translations/hu.json new file mode 100644 index 00000000000..c4a6e63030c --- /dev/null +++ b/homeassistant/components/tile/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + }, + "title": "Tile konfigur\u00e1l\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Inakt\u00edv Tile-ok megjelen\u00edt\u00e9se" + }, + "title": "Tile konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/id.json b/homeassistant/components/tile/translations/id.json new file mode 100644 index 00000000000..5b5c710594d --- /dev/null +++ b/homeassistant/components/tile/translations/id.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Konfigurasi Tile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Tampilkan Tile yang tidak aktif" + }, + "title": "Konfigurasi Tile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 4615e9e046c..08195e6dd3d 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -4,11 +4,10 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_DISPLAY_OPTIONS from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util @@ -47,7 +46,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class TimeDateSensor(Entity): +class TimeDateSensor(SensorEntity): """Implementation of a Time and Date sensor.""" def __init__(self, hass, option_type): diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index b0ff60bbcae..2ff408dcd81 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Dict, Optional import voluptuous as vol @@ -165,7 +164,7 @@ class TimerStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: Dict) -> Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" data = self.CREATE_SCHEMA(data) # make duration JSON serializeable @@ -173,11 +172,11 @@ class TimerStorageCollection(collection.StorageCollection): return data @callback - def _get_suggested_id(self, info: Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: Dict) -> Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" data = {**data, **self.UPDATE_SCHEMA(update_data)} # make duration JSON serializeable @@ -189,18 +188,18 @@ class TimerStorageCollection(collection.StorageCollection): class Timer(RestoreEntity): """Representation of a timer.""" - def __init__(self, config: Dict): + def __init__(self, config: dict): """Initialize a timer.""" self._config: dict = config self.editable: bool = True self._state: str = STATUS_IDLE self._duration = cv.time_period_str(config[CONF_DURATION]) - self._remaining: Optional[timedelta] = None - self._end: Optional[datetime] = None + self._remaining: timedelta | None = None + self._end: datetime | None = None self._listener = None @classmethod - def from_yaml(cls, config: Dict) -> Timer: + def from_yaml(cls, config: dict) -> Timer: """Return entity instance initialized from yaml storage.""" timer = cls(config) timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) @@ -233,7 +232,7 @@ class Timer(RestoreEntity): return self._state @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = { ATTR_DURATION: _format_timedelta(self._duration), @@ -247,7 +246,7 @@ class Timer(RestoreEntity): return attrs @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return unique id for the entity.""" return self._config[CONF_ID] @@ -328,7 +327,9 @@ class Timer(RestoreEntity): if self._state != STATUS_ACTIVE: return - self._listener = None + if self._listener: + self._listener() + self._listener = None self._state = STATUS_IDLE self._end = None self._remaining = None @@ -348,7 +349,7 @@ class Timer(RestoreEntity): self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) self.async_write_ha_state() - async def async_update_config(self, config: Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" self._config = config self._duration = cv.time_period_str(config[CONF_DURATION]) diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py index 71abb0bfd71..377f8a1dda2 100644 --- a/homeassistant/components/timer/reproduce_state.py +++ b/homeassistant/components/timer/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Timer state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State @@ -27,8 +29,8 @@ async def _async_reproduce_state( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -69,8 +71,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Timer states.""" await asyncio.gather( diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index f731b912d65..88471a86c27 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -6,10 +6,9 @@ from requests import HTTPError from tmb import IBus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -63,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class TMBSensor(Entity): +class TMBSensor(SensorEntity): """Implementation of a TMB line/stop Sensor.""" def __init__(self, ibus_client, stop, line, name): @@ -101,7 +100,7 @@ class TMBSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the last update.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index fde26acf604..26e0ead680a 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -109,7 +109,7 @@ class TodSensor(BinarySensorEntity): return self._next_update @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_AFTER: self.after.astimezone(self.hass.config.time_zone).isoformat(), diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 1188831c26d..976462c95fa 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -226,15 +226,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -def _parse_due_date(data: dict) -> datetime: +def _parse_due_date(data: dict, gmt_string) -> datetime: """Parse the due date dict into a datetime object.""" # Add time information to date only strings. if len(data["date"]) == 10: data["date"] += "T00:00:00" - # If there is no timezone provided, use UTC. - if data["timezone"] is None: - data["date"] += "Z" - return dt.parse_datetime(data["date"]) + if dt.parse_datetime(data["date"]).tzinfo is None: + data["date"] += gmt_string + return dt.as_utc(dt.parse_datetime(data["date"])) class TodoistProjectDevice(CalendarEventDevice): @@ -285,7 +284,7 @@ class TodoistProjectDevice(CalendarEventDevice): return await self.data.async_get_events(hass, start_date, end_date) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if self.data.event is None: # No tasks, we don't REALLY need to show anything. @@ -407,7 +406,9 @@ class TodoistProjectData: # Generally speaking, that means right now. task[START] = dt.utcnow() if data[DUE] is not None: - task[END] = _parse_due_date(data[DUE]) + task[END] = _parse_due_date( + data[DUE], self._api.state["user"]["tz_info"]["gmt_string"] + ) if self._due_date_days is not None and ( task[END] > dt.utcnow() + self._due_date_days @@ -529,9 +530,19 @@ class TodoistProjectData: for task in project_task_data: if task["due"] is None: continue - due_date = _parse_due_date(task["due"]) + due_date = _parse_due_date( + task["due"], self._api.state["user"]["tz_info"]["gmt_string"] + ) + midnight = dt.as_utc( + dt.parse_datetime( + due_date.strftime("%Y-%m-%d") + + "T00:00:00" + + self._api.state["user"]["tz_info"]["gmt_string"] + ) + ) + if start_date < due_date < end_date: - if due_date.hour == 0 and due_date.minute == 0: + if due_date == midnight: # If the due date has no time data, return just the date so that it # will render correctly as an all day event on a calendar. due_date_value = due_date.strftime("%Y-%m-%d") diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index 17b2d1010bf..45713dd8f77 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -7,10 +7,9 @@ from VL53L1X2 import VL53L1X # pylint: disable=import-error import voluptuous as vol from homeassistant.components import rpi_gpio -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, LENGTH_MILLIMETERS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity CONF_I2C_ADDRESS = "i2c_address" CONF_I2C_BUS = "i2c_bus" @@ -65,7 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class VL53L1XSensor(Entity): +class VL53L1XSensor(SensorEntity): """Implementation of VL53L1X sensor.""" def __init__(self, vl53l1x_sensor, name, unit, i2c_address): diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index d1753ad4d02..87c68b5addb 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -15,7 +15,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, @@ -27,7 +26,7 @@ from .const import CONF_AGREEMENT_ID, CONF_MIGRATE, DEFAULT_SCAN_INTERVAL, DOMAI from .coordinator import ToonDataUpdateCoordinator from .oauth2 import register_oauth2_implementations -ENTITY_COMPONENTS = { +PLATFORMS = { BINARY_SENSOR_DOMAIN, CLIMATE_DOMAIN, SENSOR_DOMAIN, @@ -98,10 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.toon.activate_agreement( agreement_id=entry.data[CONF_AGREEMENT_ID] ) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator @@ -119,9 +115,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Spin up the platforms - for component in ENTITY_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) # If Home Assistant is already in a running state, register the webhook @@ -146,8 +142,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *( - hass.config_entries.async_forward_entry_unload(entry, component) - for component in ENTITY_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ) ) ) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index fe14435f2ab..6651806a21c 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -1,5 +1,5 @@ """Support for Toon binary sensors.""" -from typing import Optional +from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry @@ -84,7 +84,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorEntity): return BINARY_SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS] @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return the status of the binary sensor.""" section = getattr( self.coordinator.data, BINARY_SENSOR_ENTITIES[self.key][ATTR_SECTION] diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index ba3ef8ee807..db2bed47f51 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -1,5 +1,7 @@ """Support for Toon thermostat.""" -from typing import Any, Dict, List, Optional +from __future__ import annotations + +from typing import Any from toonapi import ( ACTIVE_STATE_AWAY, @@ -61,12 +63,12 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): return HVAC_MODE_HEAT @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return [HVAC_MODE_HEAT] @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation.""" if self.coordinator.data.thermostat.heating: return CURRENT_HVAC_HEAT @@ -78,7 +80,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): return TEMP_CELSIUS @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" mapping = { ACTIVE_STATE_AWAY: PRESET_AWAY, @@ -89,17 +91,17 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): return mapping.get(self.coordinator.data.thermostat.active_state) @property - def preset_modes(self) -> List[str]: + def preset_modes(self) -> list[str]: """Return a list of available preset modes.""" return [PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_SLEEP] @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self.coordinator.data.thermostat.current_display_temperature @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self.coordinator.data.thermostat.current_setpoint @@ -114,7 +116,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateEntity): return DEFAULT_MAX_TEMP @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the current state of the burner.""" return {"heating_type": self.coordinator.data.agreement.heating_type} diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 1e1739e85df..bc673d2d181 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the Toon component.""" +from __future__ import annotations + import logging -from typing import Any, Dict, List, Optional +from typing import Any from toonapi import Agreement, Toon, ToonError import voluptuous as vol @@ -19,15 +21,15 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): DOMAIN = DOMAIN VERSION = 2 - agreements: Optional[List[Agreement]] = None - data: Optional[Dict[str, Any]] = None + agreements: list[Agreement] | None = None + data: dict[str, Any] | None = None @property def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) - async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> dict[str, Any]: """Test connection and load up agreements.""" self.data = data @@ -46,8 +48,8 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self.async_step_agreement() async def async_step_import( - self, config: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, config: dict[str, Any] | None = None + ) -> dict[str, Any]: """Start a configuration flow based on imported data. This step is merely here to trigger "discovery" when the `toon` @@ -63,8 +65,8 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self.async_step_user() async def async_step_agreement( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """Select Toon agreement to add.""" if len(self.agreements) == 1: return await self._create_entry(self.agreements[0]) @@ -85,7 +87,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): agreement_index = agreements_list.index(user_input[CONF_AGREEMENT]) return await self._create_entry(self.agreements[agreement_index]) - async def _create_entry(self, agreement: Agreement) -> Dict[str, Any]: + async def _create_entry(self, agreement: Agreement) -> dict[str, Any]: if CONF_MIGRATE in self.context: await self.hass.config_entries.async_remove(self.context[CONF_MIGRATE]) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index a1afaf9f9b0..d1a8b702438 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -5,7 +5,11 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PROBLEM, ) -from homeassistant.components.sensor import DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -46,7 +50,7 @@ BINARY_SENSOR_ENTITIES = { ATTR_MEASUREMENT: "boiler_module_connected", ATTR_INVERTED: False, ATTR_DEVICE_CLASS: DEVICE_CLASS_CONNECTIVITY, - ATTR_ICON: "mdi:check-network-outline", + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "thermostat_info_burner_info_1": { @@ -184,7 +188,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "average", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: "mdi:power-plug", + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "power_average_daily": { @@ -192,8 +196,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_average", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "power_daily_cost": { @@ -210,8 +214,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, }, "power_meter_reading": { @@ -219,8 +223,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "power_meter_reading_low": { @@ -228,8 +232,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "power_value": { @@ -238,7 +242,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: "mdi:power-plug", + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, }, "solar_meter_reading_produced": { @@ -246,8 +250,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_produced_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "solar_meter_reading_low_produced": { @@ -255,8 +259,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_produced_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "solar_value": { @@ -265,7 +269,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current_solar", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: "mdi:solar-power", + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, }, "solar_maximum": { @@ -273,8 +277,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_max_solar", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:solar-power", + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, }, "solar_produced": { @@ -283,7 +287,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "current_produced", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: "mdi:solar-power", + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, }, "power_usage_day_produced_solar": { @@ -291,8 +295,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_produced_solar", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:solar-power", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: True, }, "power_usage_day_to_grid_usage": { @@ -300,8 +304,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_to_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:solar-power", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "power_usage_day_from_grid_usage": { @@ -309,8 +313,8 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_from_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:power-plug", + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "solar_average_produced": { @@ -319,7 +323,7 @@ SENSOR_ENTITIES = { ATTR_MEASUREMENT: "average_produced", ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_ICON: "mdi:solar-power", + ATTR_ICON: None, ATTR_DEFAULT_ENABLED: False, }, "thermostat_info_current_modulation_level": { diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 359cb5b0ffb..069bd58d922 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -1,7 +1,8 @@ """Provides the Toon DataUpdateCoordinator.""" +from __future__ import annotations + import logging import secrets -from typing import Optional from toonapi import Status, Toon, ToonError @@ -50,7 +51,7 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): for update_callback in self._listeners: update_callback() - async def register_webhook(self, event: Optional[Event] = None) -> None: + async def register_webhook(self, event: Event | None = None) -> None: """Register a webhook with Toon to get live updates.""" if CONF_WEBHOOK_ID not in self.entry.data: data = {**self.entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} @@ -124,7 +125,7 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): except ToonError as err: _LOGGER.error("Could not process data received from Toon webhook - %s", err) - async def unregister_webhook(self, event: Optional[Event] = None) -> None: + async def unregister_webhook(self, event: Event | None = None) -> None: """Remove / Unregister webhook for toon.""" _LOGGER.debug( "Unregistering Toon webhook (%s)", self.entry.data[CONF_WEBHOOK_ID] diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index edcbed369a3..8aee2fe27e1 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -1,5 +1,7 @@ """DataUpdate Coordinator, and base Entity and Device models for Toon.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -31,7 +33,7 @@ class ToonEntity(CoordinatorEntity): return self._name @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the mdi icon of the entity.""" return self._icon @@ -45,7 +47,7 @@ class ToonDisplayDeviceEntity(ToonEntity): """Defines a Toon display device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this thermostat.""" agreement = self.coordinator.data.agreement model = agreement.display_hardware_version.rpartition("/")[0] @@ -63,7 +65,7 @@ class ToonElectricityMeterDeviceEntity(ToonEntity): """Defines a Electricity Meter device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -77,7 +79,7 @@ class ToonGasMeterDeviceEntity(ToonEntity): """Defines a Gas Meter device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -91,7 +93,7 @@ class ToonWaterMeterDeviceEntity(ToonEntity): """Defines a Water Meter device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -105,7 +107,7 @@ class ToonSolarDeviceEntity(ToonEntity): """Defines a Solar Device device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -119,7 +121,7 @@ class ToonBoilerModuleDeviceEntity(ToonEntity): """Defines a Boiler Module device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { @@ -134,7 +136,7 @@ class ToonBoilerDeviceEntity(ToonEntity): """Defines a Boiler device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id return { diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py index e3a83583ac6..7539224ebba 100644 --- a/homeassistant/components/toon/oauth2.py +++ b/homeassistant/components/toon/oauth2.py @@ -1,5 +1,7 @@ """OAuth2 implementations for Toon.""" -from typing import Any, Optional, cast +from __future__ import annotations + +from typing import Any, cast from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow @@ -55,7 +57,7 @@ class ToonLocalOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implemen client_secret: str, name: str, tenant_id: str, - issuer: Optional[str] = None, + issuer: str | None = None, ): """Local Toon Oauth Implementation.""" self._name = name diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 52d3a68f2c1..36f5dedde3d 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,6 +1,7 @@ """Support for Toon sensors.""" -from typing import Optional +from __future__ import annotations +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -109,7 +110,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class ToonSensor(ToonEntity): +class ToonSensor(ToonEntity, SensorEntity): """Defines a Toon sensor.""" def __init__(self, coordinator: ToonDataUpdateCoordinator, *, key: str) -> None: @@ -132,7 +133,7 @@ class ToonSensor(ToonEntity): return f"{DOMAIN}_{agreement_id}_sensor_{self.key}" @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the sensor.""" section = getattr( self.coordinator.data, SENSOR_ENTITIES[self.key][ATTR_SECTION] @@ -140,12 +141,12 @@ class ToonSensor(ToonEntity): return getattr(section, SENSOR_ENTITIES[self.key][ATTR_MEASUREMENT]) @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return SENSOR_ENTITIES[self.key][ATTR_UNIT_OF_MEASUREMENT] @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the device class.""" return SENSOR_ENTITIES[self.key][ATTR_DEVICE_CLASS] diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json new file mode 100644 index 00000000000..cd832522870 --- /dev/null +++ b/homeassistant/components/toon/translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", + "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/id.json b/homeassistant/components/toon/translations/id.json new file mode 100644 index 00000000000..6e9d4a76683 --- /dev/null +++ b/homeassistant/components/toon/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perjanjian yang dipilih sudah dikonfigurasi.", + "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "no_agreements": "Akun ini tidak memiliki tampilan Toon.", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "unknown_authorize_url_generation": "Kesalahan tidak dikenal ketika menghasilkan URL otorisasi." + }, + "step": { + "agreement": { + "data": { + "agreement": "Persetujuan" + }, + "description": "Pilih alamat persetujuan yang ingin ditambahkan.", + "title": "Pilih persetujuan Anda" + }, + "pick_implementation": { + "title": "Pilih penyewa Anda untuk diautentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index faed1fe74d7..e36adba2ffb 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uc120\ud0dd\ub41c \uc57d\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d URL \uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "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.", "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index a0cda915172..69ae8aa127f 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -8,6 +8,18 @@ "no_agreements": "Dit account heeft geen Toon schermen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." + }, + "step": { + "agreement": { + "data": { + "agreement": "Overeenkomst" + }, + "description": "Selecteer het overeenkomstadres dat u wilt toevoegen.", + "title": "Kies uw overeenkomst" + }, + "pick_implementation": { + "title": "Kies uw tenant om mee te authenticeren" + } } } } \ No newline at end of file diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 4b52a565d87..156259adccb 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -4,11 +4,10 @@ import re import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity API_PATH = "/api/torque" @@ -106,7 +105,7 @@ class TorqueReceiveDataView(HomeAssistantView): return "OK!" -class TorqueSensor(Entity): +class TorqueSensor(SensorEntity): """Representation of a Torque sensor.""" def __init__(self, name, unit): diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 179d60b794a..8ef223c49a5 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -82,9 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = client - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index affff382365..c277198b683 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -44,7 +44,7 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): self._location_id = location_id self._client = client self._state = None - self._device_state_attributes = {} + self._extra_state_attributes = {} @property def name(self): @@ -62,9 +62,9 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" - return self._device_state_attributes + return self._extra_state_attributes def update(self): """Return the state of the device.""" @@ -109,7 +109,7 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): state = None self._state = state - self._device_state_attributes = attr + self._extra_state_attributes = attr def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 6bee603d1b1..ef02c5d1fd3 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -73,7 +73,7 @@ class TotalConnectBinarySensor(BinarySensorEntity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attributes = { "zone_id": self._zone_id, diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 27fa4203a42..122b1ad88b4 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import CONF_USERCODES, DOMAIN # pylint: disable=unused-import +from .const import CONF_USERCODES, DOMAIN CONF_LOCATION = "location" diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 3fb5bb8f3e1..57dbaf77364 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -14,6 +14,7 @@ } }, "reauth_confirm": { + "description": "Total Connect muss dein Konto neu authentifizieren", "title": "Integration erneut authentifizieren" }, "user": { diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 090d9271dee..85797fa901e 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -9,6 +9,9 @@ }, "step": { "locations": { + "data": { + "location": "Localizaci\u00f3n" + }, "description": "Ingrese el c\u00f3digo de usuario para este usuario en esta ubicaci\u00f3n", "title": "C\u00f3digos de usuario de ubicaci\u00f3n" }, diff --git a/homeassistant/components/totalconnect/translations/he.json b/homeassistant/components/totalconnect/translations/he.json new file mode 100644 index 00000000000..ed07845a182 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "locations": { + "data": { + "location": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + }, + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index dee4ed9ee0f..6002f056635 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -1,6 +1,21 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { + "locations": { + "data": { + "location": "Elhelyezked\u00e9s" + } + }, + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json new file mode 100644 index 00000000000..c1bdf664994 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "usercode": "Kode pengguna tidak valid untuk pengguna ini di lokasi ini" + }, + "step": { + "locations": { + "data": { + "location": "Lokasi" + }, + "description": "Masukkan kode pengguna untuk pengguna ini di lokasi ini", + "title": "Lokasi Kode Pengguna" + }, + "reauth_confirm": { + "description": "Total Connect perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ko.json b/homeassistant/components/totalconnect/translations/ko.json index c074472b8f4..354522154b5 100644 --- a/homeassistant/components/totalconnect/translations/ko.json +++ b/homeassistant/components/totalconnect/translations/ko.json @@ -1,12 +1,25 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "usercode": "\uc774 \uc704\uce58\uc758 \ud574\ub2f9 \uc0ac\uc6a9\uc790\uc5d0 \ub300\ud55c \uc0ac\uc6a9\uc790 \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { + "locations": { + "data": { + "location": "\uc704\uce58" + }, + "description": "\uc774 \uc704\uce58\uc758 \ud574\ub2f9 \uc0ac\uc6a9\uc790\uc5d0 \ub300\ud55c \uc0ac\uc6a9\uc790 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "\uc704\uce58 \uc0ac\uc6a9\uc790 \ucf54\ub4dc" + }, + "reauth_confirm": { + "description": "Total Connect\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index 94d8e3ac01e..de20d40bee6 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -1,19 +1,23 @@ { "config": { "abort": { - "already_configured": "Account al geconfigureerd", + "already_configured": "Account is al geconfigureerd", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "usercode": "Gebruikerscode niet geldig voor deze gebruiker op deze locatie" }, "step": { "locations": { "data": { "location": "Locatie" - } + }, + "description": "Voer de gebruikerscode voor deze gebruiker op deze locatie in", + "title": "Locatie gebruikerscodes" }, "reauth_confirm": { + "description": "Total Connect moet uw account opnieuw verifi\u00ebren", "title": "Verifieer de integratie opnieuw" }, "user": { diff --git a/homeassistant/components/totalconnect/translations/pt.json b/homeassistant/components/totalconnect/translations/pt.json index 3c17682089a..11ac8262267 100644 --- a/homeassistant/components/totalconnect/translations/pt.json +++ b/homeassistant/components/totalconnect/translations/pt.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "locations": { + "data": { + "location": "Localiza\u00e7\u00e3o" + } + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index 0f067541dec..a32f92b7b58 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -23,7 +23,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Total Connect" } diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 1aca4bf7edc..b9318cf3fdd 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -1,6 +1,7 @@ """Common code for tplink.""" +from __future__ import annotations + import logging -from typing import List from pyHS100 import ( Discover, @@ -30,7 +31,7 @@ class SmartDevices: """Hold different kinds of devices.""" def __init__( - self, lights: List[SmartDevice] = None, switches: List[SmartDevice] = None + self, lights: list[SmartDevice] = None, switches: list[SmartDevice] = None ): """Initialize device holder.""" self._lights = lights or [] diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index ceb0944efe6..88c24d7cf30 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -1,9 +1,12 @@ """Support for TPLink lights.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging +import re import time -from typing import Any, Dict, NamedTuple, Tuple, cast +from typing import Any, NamedTuple, cast from pyHS100 import SmartBulb, SmartDeviceException @@ -58,6 +61,21 @@ LIGHT_SYSINFO_IS_COLOR = "is_color" MAX_ATTEMPTS = 300 SLEEP_TIME = 2 +TPLINK_KELVIN = { + "LB130": (2500, 9000), + "LB120": (2700, 6500), + "LB230": (2500, 9000), + "KB130": (2500, 9000), + "KL130": (2500, 9000), + "KL125": (2500, 6500), + r"KL120\(EU\)": (2700, 6500), + r"KL120\(US\)": (2700, 5000), + r"KL430\(US\)": (2500, 9000), +} + +FALLBACK_MIN_COLOR = 2700 +FALLBACK_MAX_COLOR = 5000 + async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): """Set up lights.""" @@ -88,7 +106,7 @@ class LightState(NamedTuple): state: bool brightness: int color_temp: float - hs: Tuple[int, int] + hs: tuple[int, int] def to_param(self): """Return a version that we can send to the bulb.""" @@ -109,7 +127,7 @@ class LightState(NamedTuple): class LightFeatures(NamedTuple): """Light features.""" - sysinfo: Dict[str, Any] + sysinfo: dict[str, Any] mac: str alias: str model: str @@ -163,7 +181,7 @@ class TPLinkSmartBulb(LightEntity): return self._is_available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._emeter_params @@ -267,6 +285,19 @@ class TPLinkSmartBulb(LightEntity): """Flag supported features.""" return self._light_features.supported_features + def _get_valid_temperature_range(self): + """Return the device-specific white temperature range (in Kelvin). + + :return: White temperature range in Kelvin (minimum, maximum) + """ + model = self.smartbulb.sys_info[LIGHT_SYSINFO_MODEL] + for obj, temp_range in TPLINK_KELVIN.items(): + if re.match(obj, model): + return temp_range + # pyHS100 is abandoned, but some bulb definitions aren't present + # use "safe" values for something that advertises color temperature + return FALLBACK_MIN_COLOR, FALLBACK_MAX_COLOR + def _get_light_features(self): """Determine all supported features in one go.""" sysinfo = self.smartbulb.sys_info @@ -283,9 +314,7 @@ class TPLinkSmartBulb(LightEntity): supported_features += SUPPORT_BRIGHTNESS if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP): supported_features += SUPPORT_COLOR_TEMP - # Have to make another api request here in - # order to not re-implement pyHS100 here - max_range, min_range = self.smartbulb.valid_temperature_range + max_range, min_range = self._get_valid_temperature_range() min_mireds = kelvin_to_mired(min_range) max_mireds = kelvin_to_mired(max_range) if sysinfo.get(LIGHT_SYSINFO_IS_COLOR): @@ -318,12 +347,12 @@ class TPLinkSmartBulb(LightEntity): light_state_params[LIGHT_STATE_BRIGHTNESS] ) - if light_features.supported_features & SUPPORT_COLOR_TEMP: - if ( - light_state_params.get(LIGHT_STATE_COLOR_TEMP) is not None - and light_state_params[LIGHT_STATE_COLOR_TEMP] != 0 - ): - color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP]) + if ( + light_features.supported_features & SUPPORT_COLOR_TEMP + and light_state_params.get(LIGHT_STATE_COLOR_TEMP) is not None + and light_state_params[LIGHT_STATE_COLOR_TEMP] != 0 + ): + color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP]) if light_features.supported_features & SUPPORT_COLOR: hue_saturation = ( diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 23000fe7b59..4d7dce37447 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,5 +1,6 @@ """Support for TPLink HS100/HS110/HS200 smart switch.""" import asyncio +from contextlib import suppress import logging import time @@ -100,7 +101,7 @@ class SmartPlugSwitch(SwitchEntity): self.smartplug.turn_off() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._emeter_params @@ -151,13 +152,10 @@ class SmartPlugSwitch(SwitchEntity): ) emeter_statics = self.smartplug.get_emeter_daily() - try: + with suppress(KeyError): # Device returned no daily history self._emeter_params[ATTR_TODAY_ENERGY_KWH] = "{:.3f}".format( emeter_statics[int(time.strftime("%e"))] ) - except KeyError: - # Device returned no daily history - pass return True except (SmartDeviceException, OSError) as ex: if update_attempt == 0: diff --git a/homeassistant/components/tplink/translations/hu.json b/homeassistant/components/tplink/translations/hu.json new file mode 100644 index 00000000000..ab799e90c74 --- /dev/null +++ b/homeassistant/components/tplink/translations/hu.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/id.json b/homeassistant/components/tplink/translations/id.json new file mode 100644 index 00000000000..66d510de4ed --- /dev/null +++ b/homeassistant/components/tplink/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan perangkat cerdas TP-Link?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/ko.json b/homeassistant/components/tplink/translations/ko.json index e1ff7eff372..a1bfb59ca07 100644 --- a/homeassistant/components/tplink/translations/ko.json +++ b/homeassistant/components/tplink/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/tplink/translations/nl.json b/homeassistant/components/tplink/translations/nl.json index f6a8dbbe02a..362645d9f19 100644 --- a/homeassistant/components/tplink/translations/nl.json +++ b/homeassistant/components/tplink/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Geen TP-Link apparaten gevonden op het netwerk.", - "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie is nodig." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index cdb20339510..d558129e323 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -345,7 +345,7 @@ class TraccarEntity(TrackerEntity, RestoreEntity): return self._battery @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific attributes.""" return self._attributes diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index a14c446e673..c4fc027d059 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: \" {webhook_url} \" \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/id.json b/homeassistant/components/traccar/translations/id.json new file mode 100644 index 00000000000..573b73570c2 --- /dev/null +++ b/homeassistant/components/traccar/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan fitur webhook di Traccar.\n\nGunakan URL berikut: {webhook_url}`\n\nBaca [dokumentasi]({docs_url}) untuk detail lebih lanjut." + }, + "step": { + "user": { + "description": "Yakin ingin menyiapkan Traccar?", + "title": "Siapkan Traccar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/ko.json b/homeassistant/components/traccar/translations/ko.json index 04e13a9aa6f..aa5e2a65736 100644 --- a/homeassistant/components/traccar/translations/ko.json +++ b/homeassistant/components/traccar/translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 URL \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar\uc5d0\uc11c \uc6f9 \ud6c5\uc744 \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c\uc758 URL \uc8fc\uc18c\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { - "description": "Traccar \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Traccar\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Traccar \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py new file mode 100644 index 00000000000..eca22a56da8 --- /dev/null +++ b/homeassistant/components/trace/__init__.py @@ -0,0 +1,131 @@ +"""Support for script and automation tracing and debugging.""" +from __future__ import annotations + +import datetime as dt +from itertools import count +from typing import Any, Deque + +from homeassistant.core import Context +from homeassistant.helpers.trace import ( + TraceElement, + script_execution_get, + trace_id_get, + trace_id_set, + trace_set_child_id, +) +import homeassistant.util.dt as dt_util + +from . import websocket_api +from .const import DATA_TRACE, STORED_TRACES +from .utils import LimitedSizeDict + +DOMAIN = "trace" + + +async def async_setup(hass, config): + """Initialize the trace integration.""" + hass.data[DATA_TRACE] = {} + websocket_api.async_setup(hass) + return True + + +def async_store_trace(hass, trace): + """Store a trace if its item_id is valid.""" + key = trace.key + if key[1]: + traces = hass.data[DATA_TRACE] + if key not in traces: + traces[key] = LimitedSizeDict(size_limit=STORED_TRACES) + traces[key][trace.run_id] = trace + + +class ActionTrace: + """Base container for an script or automation trace.""" + + _run_ids = count(0) + + def __init__( + self, + key: tuple[str, str], + config: dict[str, Any], + blueprint_inputs: dict[str, Any], + context: Context, + ): + """Container for script trace.""" + self._trace: dict[str, Deque[TraceElement]] | None = None + self._config: dict[str, Any] = config + self._blueprint_inputs: dict[str, Any] = blueprint_inputs + self.context: Context = context + self._error: Exception | None = None + self._state: str = "running" + self._script_execution: str | None = None + self.run_id: str = str(next(self._run_ids)) + self._timestamp_finish: dt.datetime | None = None + self._timestamp_start: dt.datetime = dt_util.utcnow() + self.key: tuple[str, str] = key + if trace_id_get(): + trace_set_child_id(self.key, self.run_id) + trace_id_set((key, self.run_id)) + + def set_trace(self, trace: dict[str, Deque[TraceElement]]) -> None: + """Set trace.""" + self._trace = trace + + def set_error(self, ex: Exception) -> None: + """Set error.""" + self._error = ex + + def finished(self) -> None: + """Set finish time.""" + self._timestamp_finish = dt_util.utcnow() + self._state = "stopped" + self._script_execution = script_execution_get() + + def as_dict(self) -> dict[str, Any]: + """Return dictionary version of this ActionTrace.""" + + result = self.as_short_dict() + + traces = {} + if self._trace: + for key, trace_list in self._trace.items(): + traces[key] = [item.as_dict() for item in trace_list] + + result.update( + { + "trace": traces, + "config": self._config, + "blueprint_inputs": self._blueprint_inputs, + "context": self.context, + } + ) + if self._error is not None: + result["error"] = str(self._error) + return result + + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this ActionTrace.""" + + last_step = None + + if self._trace: + last_step = list(self._trace)[-1] + + result = { + "last_step": last_step, + "run_id": self.run_id, + "state": self._state, + "script_execution": self._script_execution, + "timestamp": { + "start": self._timestamp_start, + "finish": self._timestamp_finish, + }, + "domain": self.key[0], + "item_id": self.key[1], + } + if self._error is not None: + result["error"] = str(self._error) + if last_step is not None: + result["last_step"] = last_step + + return result diff --git a/homeassistant/components/trace/const.py b/homeassistant/components/trace/const.py new file mode 100644 index 00000000000..05942d7ee4d --- /dev/null +++ b/homeassistant/components/trace/const.py @@ -0,0 +1,4 @@ +"""Shared constants for script and automation tracing and debugging.""" + +DATA_TRACE = "trace" +STORED_TRACES = 5 # Stored traces per script or automation diff --git a/homeassistant/components/trace/manifest.json b/homeassistant/components/trace/manifest.json new file mode 100644 index 00000000000..cdd857d00d3 --- /dev/null +++ b/homeassistant/components/trace/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "trace", + "name": "Trace", + "documentation": "https://www.home-assistant.io/integrations/automation", + "codeowners": [ + "@home-assistant/core" + ], + "quality_scale": "internal" +} diff --git a/homeassistant/components/trace/utils.py b/homeassistant/components/trace/utils.py new file mode 100644 index 00000000000..7e804724c55 --- /dev/null +++ b/homeassistant/components/trace/utils.py @@ -0,0 +1,43 @@ +"""Helpers for script and automation tracing and debugging.""" +from collections import OrderedDict +from datetime import timedelta +from typing import Any + +from homeassistant.helpers.json import JSONEncoder as HAJSONEncoder + + +class LimitedSizeDict(OrderedDict): + """OrderedDict limited in size.""" + + def __init__(self, *args, **kwds): + """Initialize OrderedDict limited in size.""" + self.size_limit = kwds.pop("size_limit", None) + OrderedDict.__init__(self, *args, **kwds) + self._check_size_limit() + + def __setitem__(self, key, value): + """Set item and check dict size.""" + OrderedDict.__setitem__(self, key, value) + self._check_size_limit() + + def _check_size_limit(self): + """Check dict size and evict items in FIFO order if needed.""" + if self.size_limit is not None: + while len(self) > self.size_limit: + self.popitem(last=False) + + +class TraceJSONEncoder(HAJSONEncoder): + """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" + + def default(self, o: Any) -> Any: + """Convert certain objects. + + Fall back to repr(o). + """ + if isinstance(o, timedelta): + return {"__type": str(type(o)), "total_seconds": o.total_seconds()} + try: + return super().default(o) + except TypeError: + return {"__type": str(type(o)), "repr": repr(o)} diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py new file mode 100644 index 00000000000..17f3dc7860d --- /dev/null +++ b/homeassistant/components/trace/websocket_api.py @@ -0,0 +1,300 @@ +"""Websocket API for automation.""" +import json + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import ( + DATA_DISPATCHER, + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.script import ( + SCRIPT_BREAKPOINT_HIT, + SCRIPT_DEBUG_CONTINUE_ALL, + breakpoint_clear, + breakpoint_clear_all, + breakpoint_list, + breakpoint_set, + debug_continue, + debug_step, + debug_stop, +) + +from .const import DATA_TRACE +from .utils import TraceJSONEncoder + +# mypy: allow-untyped-calls, allow-untyped-defs + +TRACE_DOMAINS = ("automation", "script") + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the websocket API.""" + websocket_api.async_register_command(hass, websocket_trace_get) + websocket_api.async_register_command(hass, websocket_trace_list) + websocket_api.async_register_command(hass, websocket_trace_contexts) + websocket_api.async_register_command(hass, websocket_breakpoint_clear) + websocket_api.async_register_command(hass, websocket_breakpoint_list) + websocket_api.async_register_command(hass, websocket_breakpoint_set) + websocket_api.async_register_command(hass, websocket_debug_continue) + websocket_api.async_register_command(hass, websocket_debug_step) + websocket_api.async_register_command(hass, websocket_debug_stop) + websocket_api.async_register_command(hass, websocket_subscribe_breakpoint_events) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/get", + vol.Required("domain"): vol.In(TRACE_DOMAINS), + vol.Required("item_id"): str, + vol.Required("run_id"): str, + } +) +def websocket_trace_get(hass, connection, msg): + """Get an script or automation trace.""" + key = (msg["domain"], msg["item_id"]) + run_id = msg["run_id"] + + try: + trace = hass.data[DATA_TRACE][key][run_id] + except KeyError: + connection.send_error( + msg["id"], websocket_api.ERR_NOT_FOUND, "The trace could not be found" + ) + return + + message = websocket_api.messages.result_message(msg["id"], trace) + + connection.send_message(json.dumps(message, cls=TraceJSONEncoder, allow_nan=False)) + + +def get_debug_traces(hass, key): + """Return a serializable list of debug traces for an script or automation.""" + traces = [] + + for trace in hass.data[DATA_TRACE].get(key, {}).values(): + traces.append(trace.as_short_dict()) + + return traces + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/list", + vol.Required("domain", "id"): vol.In(TRACE_DOMAINS), + vol.Optional("item_id", "id"): str, + } +) +def websocket_trace_list(hass, connection, msg): + """Summarize script and automation traces.""" + domain = msg["domain"] + key = (domain, msg["item_id"]) if "item_id" in msg else None + + if not key: + traces = [] + for key in hass.data[DATA_TRACE]: + if key[0] == domain: + traces.extend(get_debug_traces(hass, key)) + else: + traces = get_debug_traces(hass, key) + + connection.send_result(msg["id"], traces) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/contexts", + vol.Inclusive("domain", "id"): vol.In(TRACE_DOMAINS), + vol.Inclusive("item_id", "id"): str, + } +) +def websocket_trace_contexts(hass, connection, msg): + """Retrieve contexts we have traces for.""" + key = (msg["domain"], msg["item_id"]) if "item_id" in msg else None + + if key is not None: + values = {key: hass.data[DATA_TRACE].get(key, {})} + else: + values = hass.data[DATA_TRACE] + + contexts = { + trace.context.id: {"run_id": trace.run_id, "domain": key[0], "item_id": key[1]} + for key, traces in values.items() + for trace in traces.values() + } + + connection.send_result(msg["id"], contexts) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/debug/breakpoint/set", + vol.Required("domain"): vol.In(TRACE_DOMAINS), + vol.Required("item_id"): str, + vol.Required("node"): str, + vol.Optional("run_id"): str, + } +) +def websocket_breakpoint_set(hass, connection, msg): + """Set breakpoint.""" + key = (msg["domain"], msg["item_id"]) + node = msg["node"] + run_id = msg.get("run_id") + + if ( + SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {}) + or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT] + ): + raise HomeAssistantError("No breakpoint subscription") + + result = breakpoint_set(hass, key, run_id, node) + connection.send_result(msg["id"], result) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/debug/breakpoint/clear", + vol.Required("domain"): vol.In(TRACE_DOMAINS), + vol.Required("item_id"): str, + vol.Required("node"): str, + vol.Optional("run_id"): str, + } +) +def websocket_breakpoint_clear(hass, connection, msg): + """Clear breakpoint.""" + key = (msg["domain"], msg["item_id"]) + node = msg["node"] + run_id = msg.get("run_id") + + result = breakpoint_clear(hass, key, run_id, node) + + connection.send_result(msg["id"], result) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "trace/debug/breakpoint/list"}) +def websocket_breakpoint_list(hass, connection, msg): + """List breakpoints.""" + breakpoints = breakpoint_list(hass) + for _breakpoint in breakpoints: + _breakpoint["domain"], _breakpoint["item_id"] = _breakpoint.pop("key") + + connection.send_result(msg["id"], breakpoints) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + {vol.Required("type"): "trace/debug/breakpoint/subscribe"} +) +def websocket_subscribe_breakpoint_events(hass, connection, msg): + """Subscribe to breakpoint events.""" + + @callback + def breakpoint_hit(key, run_id, node): + """Forward events to websocket.""" + connection.send_message( + websocket_api.event_message( + msg["id"], + { + "domain": key[0], + "item_id": key[1], + "run_id": run_id, + "node": node, + }, + ) + ) + + remove_signal = async_dispatcher_connect( + hass, SCRIPT_BREAKPOINT_HIT, breakpoint_hit + ) + + @callback + def unsub(): + """Unsubscribe from breakpoint events.""" + remove_signal() + if ( + SCRIPT_BREAKPOINT_HIT not in hass.data.get(DATA_DISPATCHER, {}) + or not hass.data[DATA_DISPATCHER][SCRIPT_BREAKPOINT_HIT] + ): + breakpoint_clear_all(hass) + async_dispatcher_send(hass, SCRIPT_DEBUG_CONTINUE_ALL) + + connection.subscriptions[msg["id"]] = unsub + + connection.send_message(websocket_api.result_message(msg["id"])) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/debug/continue", + vol.Required("domain"): vol.In(TRACE_DOMAINS), + vol.Required("item_id"): str, + vol.Required("run_id"): str, + } +) +def websocket_debug_continue(hass, connection, msg): + """Resume execution of halted script or automation.""" + key = (msg["domain"], msg["item_id"]) + run_id = msg["run_id"] + + result = debug_continue(hass, key, run_id) + + connection.send_result(msg["id"], result) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/debug/step", + vol.Required("domain"): vol.In(TRACE_DOMAINS), + vol.Required("item_id"): str, + vol.Required("run_id"): str, + } +) +def websocket_debug_step(hass, connection, msg): + """Single step a halted script or automation.""" + key = (msg["domain"], msg["item_id"]) + run_id = msg["run_id"] + + result = debug_step(hass, key, run_id) + + connection.send_result(msg["id"], result) + + +@callback +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "trace/debug/stop", + vol.Required("domain"): vol.In(TRACE_DOMAINS), + vol.Required("item_id"): str, + vol.Required("run_id"): str, + } +) +def websocket_debug_stop(hass, connection, msg): + """Stop a halted script or automation.""" + key = (msg["domain"], msg["item_id"]) + run_id = msg["run_id"] + + result = debug_stop(hass, key, run_id) + + connection.send_result(msg["id"], result) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 4c984067ada..3323c54d9c2 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -148,9 +148,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): sw_version=gateway_info.firmware_version, ) - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) async def async_keep_alive(now): @@ -174,8 +174,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 7947c3ad6de..e02ac69de36 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -15,6 +15,7 @@ from .const import ( CONF_IDENTITY, CONF_IMPORT_GROUPS, CONF_KEY, + DOMAIN, KEY_SECURITY_CODE, ) @@ -28,7 +29,7 @@ class AuthError(Exception): self.code = code -@config_entries.HANDLERS.register("tradfri") +@config_entries.HANDLERS.register(DOMAIN) class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 72597637bd3..4c7cde1dfd1 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -29,7 +29,7 @@ class TradfriCover(TradfriBaseDevice, CoverEntity): self._refresh(device) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_MODEL: self._device.device_info.model_number} diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index c2bf640e2aa..455ca69147d 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,5 +1,6 @@ """Support for IKEA Tradfri sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from .base_class import TradfriBaseDevice @@ -25,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriSensor(sensor, api, gateway_id) for sensor in sensors) -class TradfriSensor(TradfriBaseDevice): +class TradfriSensor(TradfriBaseDevice, SensorEntity): """The platform class required by Home Assistant.""" def __init__(self, device, api, gateway_id): diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json index 8be065fe797..3bc4ec90e77 100644 --- a/homeassistant/components/tradfri/translations/hu.json +++ b/homeassistant/components/tradfri/translations/hu.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van." }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a gatewayhez.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_key": "Nem siker\u00fclt regisztr\u00e1lni a megadott kulcs seg\u00edts\u00e9g\u00e9vel. Ha ez t\u00f6bbsz\u00f6r megt\u00f6rt\u00e9nik, pr\u00f3b\u00e1lja meg \u00fajraind\u00edtani a gatewayt.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n." }, diff --git a/homeassistant/components/tradfri/translations/id.json b/homeassistant/components/tradfri/translations/id.json index 0671b162e1c..8ff5fe257eb 100644 --- a/homeassistant/components/tradfri/translations/id.json +++ b/homeassistant/components/tradfri/translations/id.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Bridge sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung" }, "error": { - "cannot_connect": "Tidak dapat terhubung ke gateway.", + "cannot_connect": "Gagal terhubung", "invalid_key": "Gagal mendaftar dengan kunci yang disediakan. Jika ini terus terjadi, coba mulai ulang gateway.", "timeout": "Waktu tunggu memvalidasi kode telah habis." }, @@ -12,7 +13,7 @@ "auth": { "data": { "host": "Host", - "security_code": "Kode keamanan" + "security_code": "Kode Keamanan" }, "description": "Anda dapat menemukan kode keamanan di belakang gateway Anda.", "title": "Masukkan kode keamanan" diff --git a/homeassistant/components/tradfri/translations/ko.json b/homeassistant/components/tradfri/translations/ko.json index 067a10c6490..307ba39ceaf 100644 --- a/homeassistant/components/tradfri/translations/ko.json +++ b/homeassistant/components/tradfri/translations/ko.json @@ -16,7 +16,7 @@ "security_code": "\ubcf4\uc548 \ucf54\ub4dc" }, "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ub4b7\uba74\uc5d0\uc11c \ubcf4\uc548 \ucf54\ub4dc\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "\ubcf4\uc548 \ucf54\ub4dc \uc785\ub825\ud558\uae30" + "title": "\ubcf4\uc548 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } } } diff --git a/homeassistant/components/tradfri/translations/nl.json b/homeassistant/components/tradfri/translations/nl.json index 1d0453704d0..e70e515c4d2 100644 --- a/homeassistant/components/tradfri/translations/nl.json +++ b/homeassistant/components/tradfri/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Bridge is al geconfigureerd.", - "already_in_progress": "Bridge configuratie is al in volle gang." + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang" }, "error": { - "cannot_connect": "Kan geen verbinding maken met bridge", + "cannot_connect": "Kan geen verbinding maken", "invalid_key": "Mislukt om te registreren met de meegeleverde sleutel. Als dit blijft gebeuren, probeer dan de gateway opnieuw op te starten.", "timeout": "Time-out bij validatie van code" }, diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 12f3cf73e50..37e3bd52cdc 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -6,7 +6,7 @@ import logging from pytrafikverket import TrafikverketTrain import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_API_KEY, CONF_NAME, @@ -16,7 +16,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -116,7 +115,7 @@ def next_departuredate(departure): return next_weekday(today_date, WEEKDAYS.index(departure[0])) -class TrainSensor(Entity): +class TrainSensor(SensorEntity): """Contains data about a train depature.""" def __init__(self, train_api, name, from_station, to_station, weekday, time): @@ -153,7 +152,7 @@ class TrainSensor(Entity): self._delay_in_minutes = self._state.get_delay_time() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._state is None: return None diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index bb1bad67f82..1ae090ea231 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -8,7 +8,7 @@ import aiohttp from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -24,7 +24,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -145,7 +144,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class TrafikverketWeatherStation(Entity): +class TrafikverketWeatherStation(SensorEntity): """Representation of a Trafikverket sensor.""" def __init__(self, weather_api, name, sensor_type, sensor_station): @@ -172,7 +171,7 @@ class TrafikverketWeatherStation(Entity): return self._icon @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of Trafikverket Weatherstation.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 5a37cc4d771..cb4bcceeeea 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import List import transmissionrpc from transmissionrpc.error import TransmissionError @@ -173,8 +172,8 @@ class TransmissionClient: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.tm_api = None # type: transmissionrpc.Client - self._tm_data = None # type: TransmissionData + self.tm_api: transmissionrpc.Client = None + self._tm_data: TransmissionData = None self.unsub_timer = None @property @@ -345,14 +344,14 @@ class TransmissionData: """Initialize the Transmission RPC API.""" self.hass = hass self.config = config - self.data = None # type: transmissionrpc.Session - self.available = True # type: bool - self._all_torrents = [] # type: List[transmissionrpc.Torrent] - self._api = api # type: transmissionrpc.Client - self._completed_torrents = [] # type: List[transmissionrpc.Torrent] - self._session = None # type: transmissionrpc.Session - self._started_torrents = [] # type: List[transmissionrpc.Torrent] - self._torrents = [] # type: List[transmissionrpc.Torrent] + self.data: transmissionrpc.Session = None + self.available: bool = True + self._all_torrents: list[transmissionrpc.Torrent] = [] + self._api: transmissionrpc.Client = api + self._completed_torrents: list[transmissionrpc.Torrent] = [] + self._session: transmissionrpc.Session = None + self._started_torrents: list[transmissionrpc.Torrent] = [] + self._torrents: list[transmissionrpc.Torrent] = [] @property def host(self): @@ -365,7 +364,7 @@ class TransmissionData: return f"{DATA_UPDATED}-{self.host}" @property - def torrents(self) -> List[transmissionrpc.Torrent]: + def torrents(self) -> list[transmissionrpc.Torrent]: """Get the list of torrents.""" return self._torrents diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 2a24b80be16..b00ccfc68c0 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,12 +1,14 @@ """Support for monitoring the Transmission BitTorrent client API.""" -from typing import List +from __future__ import annotations + +from contextlib import suppress from transmissionrpc.torrent import Torrent +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, DATA_RATE_MEGABYTES_PER_SECOND, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import TransmissionClient from .const import ( @@ -38,12 +40,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(dev, True) -class TransmissionSensor(Entity): +class TransmissionSensor(SensorEntity): """A base class for all Transmission sensors.""" def __init__(self, tm_client, client_name, sensor_name, sub_type=None): """Initialize the sensor.""" - self._tm_client = tm_client # type: TransmissionClient + self._tm_client: TransmissionClient = tm_client self._client_name = client_name self._name = sensor_name self._sub_type = sub_type @@ -148,7 +150,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): return "Torrents" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes, if any.""" info = _torrents_info( torrents=self._tm_client.api.torrents, @@ -168,7 +170,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): self._state = len(torrents) -def _filter_torrents(torrents: List[Torrent], statuses=None) -> List[Torrent]: +def _filter_torrents(torrents: list[Torrent], statuses=None) -> list[Torrent]: return [ torrent for torrent in torrents @@ -187,8 +189,6 @@ def _torrents_info(torrents, order, limit, statuses=None): "status": torrent.status, "id": torrent.id, } - try: + with suppress(ValueError): info["eta"] = str(torrent.eta) - except ValueError: - pass return infos diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/transmission/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index f2fd2ca79e5..22d4e18df5e 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "cannot_connect": "Nem lehet csatlakozni az \u00e1llom\u00e1shoz", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" }, "step": { @@ -21,6 +25,8 @@ "step": { "init": { "data": { + "limit": "Limit", + "order": "Sorrend", "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" } } diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json new file mode 100644 index 00000000000..a96524f5165 --- /dev/null +++ b/homeassistant/components/transmission/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "name_exists": "Nama sudah ada" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + }, + "title": "Siapkan Klien Transmission" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "limit": "Batas", + "order": "Urutan", + "scan_interval": "Frekuensi pembaruan" + }, + "title": "Konfigurasikan opsi untuk Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ko.json b/homeassistant/components/transmission/translations/ko.json index 002e374e54d..898a3710350 100644 --- a/homeassistant/components/transmission/translations/ko.json +++ b/homeassistant/components/transmission/translations/ko.json @@ -29,7 +29,7 @@ "order": "\uc21c\uc11c", "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "title": "Transmission \uc635\uc158 \uc124\uc815\ud558\uae30" + "title": "Transmission\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30" } } } diff --git a/homeassistant/components/transmission/translations/nl.json b/homeassistant/components/transmission/translations/nl.json index df9a4590e66..fcc1e05e7ab 100644 --- a/homeassistant/components/transmission/translations/nl.json +++ b/homeassistant/components/transmission/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Host is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken met host", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "name_exists": "Naam bestaat al" }, @@ -25,6 +25,8 @@ "step": { "init": { "data": { + "limit": "Limiet", + "order": "Bestel", "scan_interval": "Update frequentie" }, "title": "Configureer de opties voor Transmission" diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index 6b326bc123c..a83e19da400 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -15,7 +15,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "Transmission" } diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 2aa27f8c4b3..be76999ec3f 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from TransportNSW import TransportNSW import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_MODE, @@ -13,7 +13,6 @@ from homeassistant.const import ( TIME_MINUTES, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity ATTR_STOP_ID = "stop_id" ATTR_ROUTE = "route" @@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([TransportNSWSensor(data, stop_id, name)], True) -class TransportNSWSensor(Entity): +class TransportNSWSensor(SensorEntity): """Implementation of an Transport NSW sensor.""" def __init__(self, data, stop_id, name): @@ -87,7 +86,7 @@ class TransportNSWSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._times is not None: return { diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 8a548d8cb6b..94a6ba3a48f 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -6,7 +6,7 @@ from travispy import TravisPy from travispy.errors import TravisError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -15,7 +15,6 @@ from homeassistant.const import ( TIME_SECONDS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -94,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class TravisCISensor(Entity): +class TravisCISensor(SensorEntity): """Representation of a Travis CI sensor.""" def __init__(self, data, repo_name, user, branch, sensor_type): @@ -124,7 +123,7 @@ class TravisCISensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index b7079a3311a..52a72eca8c9 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -146,7 +146,7 @@ class SensorTrend(BinarySensorEntity): return self._device_class @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_ENTITY_ID: self._entity_id, diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 88e32ce4a46..2bb3719fe95 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.19.2"], + "requirements": ["numpy==1.20.2"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index f9b07a98595..e0f59c51e5a 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1,4 +1,6 @@ """Provide functionality for TTS.""" +from __future__ import annotations + import asyncio import functools as ft import hashlib @@ -7,7 +9,7 @@ import logging import mimetypes import os import re -from typing import Dict, Optional, cast +from typing import cast from aiohttp import web import mutagen @@ -24,6 +26,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_DESCRIPTION, CONF_NAME, CONF_PLATFORM, HTTP_BAD_REQUEST, @@ -59,7 +62,6 @@ CONF_LANG = "language" CONF_SERVICE_NAME = "service_name" CONF_TIME_MEMORY = "time_memory" -CONF_DESCRIPTION = "description" CONF_FIELDS = "fields" DEFAULT_CACHE = True @@ -243,7 +245,7 @@ async def async_setup(hass, config): return True -def _hash_options(options: Dict) -> str: +def _hash_options(options: dict) -> str: """Hashes an options dictionary.""" opts_hash = hashlib.blake2s(digest_size=5) for key, value in sorted(options.items()): @@ -512,8 +514,8 @@ class SpeechManager: class Provider: """Represent a single TTS provider.""" - hass: Optional[HomeAssistantType] = None - name: Optional[str] = None + hass: HomeAssistantType | None = None + name: str | None = None @property def default_language(self): diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 7f6ba6b26fd..1f16d131e39 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -240,9 +240,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload( - entry, component.split(".", 1)[0] + entry, platform.split(".", 1)[0] ) - for component in hass.data[DOMAIN][ENTRY_IS_SETUP] + for platform in hass.data[DOMAIN][ENTRY_IS_SETUP] ] ) ) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index b705c2c7c36..18820098a91 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -42,11 +42,11 @@ from .const import ( DEFAULT_DISCOVERY_INTERVAL, DEFAULT_QUERY_INTERVAL, DEFAULT_TUYA_MAX_COLTEMP, + DOMAIN, TUYA_DATA, TUYA_PLATFORMS, TUYA_TYPE_NOT_QUERY, ) -from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index cb6f96358c9..ab361c6ac31 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -1,6 +1,7 @@ """Support for Tuya fans.""" +from __future__ import annotations + from datetime import timedelta -from typing import Optional from homeassistant.components.fan import ( DOMAIN as SENSOR_DOMAIN, @@ -124,7 +125,7 @@ class TuyaFanDevice(TuyaDevice, FanEntity): return self._tuya.state() @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the current speed.""" if not self.is_on: return 0 diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 67a61f81a1c..6650dc754b3 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -12,6 +12,7 @@ "step": { "user": { "data": { + "country_code": "L\u00e4ndercode Ihres Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", "password": "Passwort", "username": "Benutzername" }, @@ -27,6 +28,20 @@ "error": { "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", "dev_not_found": "Ger\u00e4t nicht gefunden" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Vom Ger\u00e4t genutzter Helligkeitsbereich", + "max_kelvin": "Maximal unterst\u00fctzte Farbtemperatur in Kelvin", + "max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", + "min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin", + "min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", + "support_color": "Farbunterst\u00fctzung erzwingen", + "tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur", + "unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index b128be67087..b45148f80b8 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -2,11 +2,11 @@ "config": { "abort": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "flow_title": "Tuya konfigur\u00e1ci\u00f3", "step": { @@ -27,7 +27,7 @@ "cannot_connect": "A kapcsol\u00f3d\u00e1s nem siker\u00fclt" }, "error": { - "dev_multi_type": "A konfigur\u00e1land\u00f3 eszk\u00f6z\u00f6knek azonos t\u00edpus\u00faaknak kell lennie", + "dev_multi_type": "T\u00f6bb kiv\u00e1lasztott konfigur\u00e1land\u00f3 eszk\u00f6znek azonos t\u00edpus\u00fanak kell lennie", "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3", "dev_not_found": "Eszk\u00f6z nem tal\u00e1lhat\u00f3" }, @@ -45,18 +45,18 @@ "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet", "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g" }, - "description": "Konfigur\u00e1lja a(z) {device_type} eszk\u00f6zt \" {device_name} {device_type} \" megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", - "title": "Konfigur\u00e1lja a Tuya eszk\u00f6zt" + "description": "Konfigur\u00e1l\u00e1si lehet\u0151s\u00e9gek a(z) {device_type} t\u00edpus\u00fa `{device_name}` eszk\u00f6z megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", + "title": "Tuya eszk\u00f6z konfigur\u00e1l\u00e1sa" }, "init": { "data": { "discovery_interval": "Felfedez\u0151 eszk\u00f6z lek\u00e9rdez\u00e9si intervalluma m\u00e1sodpercben", - "list_devices": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyja \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez", - "query_device": "V\u00e1lassza ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez", + "list_devices": "V\u00e1laszd ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyd \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez", + "query_device": "V\u00e1laszd ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez", "query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben" }, - "description": "Ne \u00e1ll\u00edtsa t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", - "title": "Konfigur\u00e1lja a Tuya be\u00e1ll\u00edt\u00e1sokat" + "description": "Ne \u00e1ll\u00edtsd t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", + "title": "Tuya be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" } } } diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json new file mode 100644 index 00000000000..bb338e12752 --- /dev/null +++ b/homeassistant/components/tuya/translations/id.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "Konfigurasi Tuya", + "step": { + "user": { + "data": { + "country_code": "Kode negara akun Anda (mis., 1 untuk AS atau 86 untuk China)", + "password": "Kata Sandi", + "platform": "Aplikasi tempat akun Anda mendaftar", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial Tuya Anda.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Gagal terhubung" + }, + "error": { + "dev_multi_type": "Untuk konfigurasi sekaligus, beberapa perangkat yang dipilih harus berjenis sama", + "dev_not_config": "Jenis perangkat tidak dapat dikonfigurasi", + "dev_not_found": "Perangkat tidak ditemukan" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Rentang kecerahan yang digunakan oleh perangkat", + "curr_temp_divider": "Pembagi nilai suhu saat ini (0 = gunakan bawaan)", + "max_kelvin": "Suhu warna maksimal yang didukung dalam Kelvin", + "max_temp": "Suhu target maksimal (gunakan min dan maks = 0 untuk bawaan)", + "min_kelvin": "Suhu warna minimal yang didukung dalam Kelvin", + "min_temp": "Suhu target minimal (gunakan min dan maks = 0 untuk bawaan)", + "set_temp_divided": "Gunakan nilai suhu terbagi untuk mengirimkan perintah mengatur suhu", + "support_color": "Paksa dukungan warna", + "temp_divider": "Pembagi nilai suhu (0 = gunakan bawaan)", + "temp_step_override": "Langkah Suhu Target", + "tuya_max_coltemp": "Suhu warna maksimal yang dilaporkan oleh perangkat", + "unit_of_measurement": "Satuan suhu yang digunakan oleh perangkat" + }, + "description": "Konfigurasikan opsi untuk menyesuaikan informasi yang ditampilkan untuk perangkat {device_type} `{device_name}`", + "title": "Konfigurasi Perangkat Tuya" + }, + "init": { + "data": { + "discovery_interval": "Interval polling penemuan perangkat dalam detik", + "list_devices": "Pilih perangkat yang akan dikonfigurasi atau biarkan kosong untuk menyimpan konfigurasi", + "query_device": "Pilih perangkat yang akan menggunakan metode kueri untuk pembaruan status lebih cepat", + "query_interval": "Interval polling perangkat kueri dalam detik" + }, + "description": "Jangan atur nilai interval polling terlalu rendah karena panggilan akan gagal menghasilkan pesan kesalahan dalam log", + "title": "Konfigurasikan Opsi Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json index 81dd2689b0c..afa2541e7b9 100644 --- a/homeassistant/components/tuya/translations/ko.json +++ b/homeassistant/components/tuya/translations/ko.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -25,6 +25,41 @@ "options": { "abort": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "dev_multi_type": "\uc120\ud0dd\ud55c \uc5ec\ub7ec \uae30\uae30\ub97c \uad6c\uc131\ud558\ub824\uba74 \uc720\ud615\uc774 \ub3d9\uc77c\ud574\uc57c \ud569\ub2c8\ub2e4", + "dev_not_config": "\uae30\uae30 \uc720\ud615\uc744 \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "dev_not_found": "\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \ubc1d\uae30 \ubc94\uc704", + "curr_temp_divider": "\ud604\uc7ac \uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)", + "max_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\ub300 \uc0c9\uc628\ub3c4", + "max_temp": "\ucd5c\ub300 \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)", + "min_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\uc18c \uc0c9\uc628\ub3c4", + "min_temp": "\ucd5c\uc18c \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)", + "set_temp_divided": "\uc124\uc815 \uc628\ub3c4 \uba85\ub839\uc5d0 \ubd84\ud560\ub41c \uc628\ub3c4 \uac12 \uc0ac\uc6a9\ud558\uae30", + "support_color": "\uc0c9\uc0c1 \uc9c0\uc6d0 \uac15\uc81c \uc801\uc6a9\ud558\uae30", + "temp_divider": "\uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)", + "temp_step_override": "\ud76c\ub9dd \uc628\ub3c4 \ub2e8\uacc4", + "tuya_max_coltemp": "\uae30\uae30\uc5d0\uc11c \ubcf4\uace0\ud55c \ucd5c\ub300 \uc0c9\uc628\ub3c4", + "unit_of_measurement": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704" + }, + "description": "{device_type} `{device_name}` \uae30\uae30\uc5d0 \ub300\ud574 \ud45c\uc2dc\ub418\ub294 \uc815\ubcf4\ub97c \uc870\uc815\ud558\ub294 \uc635\uc158 \uad6c\uc131\ud558\uae30", + "title": "Tuya \uae30\uae30 \uad6c\uc131\ud558\uae30" + }, + "init": { + "data": { + "discovery_interval": "\uae30\uae30 \uac80\uc0c9 \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)", + "list_devices": "\uad6c\uc131\uc744 \uc800\uc7a5\ud558\ub824\uba74 \uad6c\uc131\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uac70\ub098 \ube44\uc6cc \ub450\uc138\uc694", + "query_device": "\ube60\ub978 \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8\ub97c \uc704\ud574 \ucffc\ub9ac \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "query_interval": "\uae30\uae30 \ucffc\ub9ac \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)" + }, + "description": "\ud3f4\ub9c1 \uac04\uaca9 \uac12\uc744 \ub108\ubb34 \ub0ae\uac8c \uc124\uc815\ud558\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ud638\ucd9c\uc5d0 \uc2e4\ud328\ud558\uace0 \ub85c\uadf8\uc5d0 \uc624\ub958 \uba54\uc2dc\uc9c0\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.", + "title": "Tuya \uc635\uc158 \uad6c\uc131\ud558\uae30" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 46a0228843b..b42922822f0 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -27,13 +27,37 @@ "cannot_connect": "Kan geen verbinding maken" }, "error": { + "dev_multi_type": "Meerdere geselecteerde apparaten om te configureren moeten van hetzelfde type zijn", + "dev_not_config": "Apparaattype kan niet worden geconfigureerd", "dev_not_found": "Apparaat niet gevonden" }, "step": { "device": { + "data": { + "brightness_range_mode": "Helderheidsbereik gebruikt door apparaat", + "curr_temp_divider": "Huidige temperatuurwaarde deler (0 = standaardwaarde)", + "max_kelvin": "Max ondersteunde kleurtemperatuur in kelvin", + "max_temp": "Maximale doeltemperatuur (gebruik min en max = 0 voor standaardwaarde)", + "min_kelvin": "Minimaal ondersteunde kleurtemperatuur in kelvin", + "min_temp": "Min. gewenste temperatuur (gebruik min en max = 0 voor standaard)", + "set_temp_divided": "Gedeelde temperatuurwaarde gebruiken voor ingestelde temperatuuropdracht", + "support_color": "Forceer kleurenondersteuning", + "temp_divider": "Temperatuurwaarde deler (0 = standaardwaarde)", + "temp_step_override": "Doeltemperatuur stap", + "tuya_max_coltemp": "Max. Kleurtemperatuur gerapporteerd door apparaat", + "unit_of_measurement": "Temperatuureenheid gebruikt door apparaat" + }, + "description": "Configureer opties om weergegeven informatie aan te passen voor {device_type} apparaat `{device_name}`", "title": "Configureer Tuya Apparaat" }, "init": { + "data": { + "discovery_interval": "Polling-interval van ontdekt apparaat in seconden", + "list_devices": "Selecteer de te configureren apparaten of laat leeg om de configuratie op te slaan", + "query_device": "Selecteer apparaat dat query-methode zal gebruiken voor snellere statusupdate", + "query_interval": "Peilinginterval van het apparaat in seconden" + }, + "description": "Stel de waarden voor het pollinginterval niet te laag in, anders zullen de oproepen geen foutmelding in het logboek genereren", "title": "Configureer Tuya opties" } } diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 742ac600c62..92ced00e733 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -43,6 +43,7 @@ "set_temp_divided": "U\u017cyj podzielonej warto\u015bci temperatury dla polecenia ustawienia temperatury", "support_color": "Wymu\u015b obs\u0142ug\u0119 kolor\u00f3w", "temp_divider": "Dzielnik warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)", + "temp_step_override": "Krok docelowej temperatury", "tuya_max_coltemp": "Maksymalna temperatura barwy raportowana przez urz\u0105dzenie", "unit_of_measurement": "Jednostka temperatury u\u017cywana przez urz\u0105dzenie" }, diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index 4babc23f2ec..f40071ba400 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -15,7 +15,7 @@ "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0430\u043a\u043a\u0430\u0443\u043d\u0442", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", "title": "Tuya" diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 06c2cb27f35..f53e4463146 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -1,7 +1,8 @@ """Support for Twente Milieu.""" +from __future__ import annotations + import asyncio from datetime import timedelta -from typing import Optional from twentemilieu import TwenteMilieu import voluptuous as vol @@ -15,11 +16,12 @@ from homeassistant.components.twentemilieu.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID +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.dispatcher import 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 SCAN_INTERVAL = timedelta(seconds=3600) @@ -27,9 +29,7 @@ SERVICE_UPDATE = "update" SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) -async def _update_twentemilieu( - hass: HomeAssistantType, unique_id: Optional[str] -) -> None: +async def _update_twentemilieu(hass: HomeAssistant, unique_id: str | None) -> None: """Update Twente Milieu.""" if unique_id is not None: twentemilieu = hass.data[DOMAIN].get(unique_id) @@ -37,16 +37,15 @@ async def _update_twentemilieu( await twentemilieu.update() async_dispatcher_send(hass, DATA_UPDATE, unique_id) else: - tasks = [] - for twentemilieu in hass.data[DOMAIN].values(): - tasks.append(twentemilieu.update()) - await asyncio.wait(tasks) + await asyncio.wait( + [twentemilieu.update() for twentemilieu in hass.data[DOMAIN].values()] + ) for uid in hass.data[DOMAIN]: async_dispatcher_send(hass, DATA_UPDATE, uid) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Twente Milieu components.""" async def update(call) -> None: @@ -59,7 +58,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 Twente Milieu from a config entry.""" session = async_get_clientsession(hass) twentemilieu = TwenteMilieu( @@ -85,7 +84,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 Twente Milieu config entry.""" await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 76c9f33b3e9..25cdd57b26d 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -1,4 +1,8 @@ """Config flow to configure the Twente Milieu integration.""" +from __future__ import annotations + +from typing import Any + from twentemilieu import ( TwenteMilieu, TwenteMilieuAddressError, @@ -7,25 +11,22 @@ from twentemilieu import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.twentemilieu.const import ( - CONF_HOUSE_LETTER, - CONF_HOUSE_NUMBER, - CONF_POST_CODE, - DOMAIN, -) from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .const import CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN -@config_entries.HANDLERS.register(DOMAIN) -class TwenteMilieuFlowHandler(ConfigFlow): + +class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Twente Milieu config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def _show_setup_form(self, errors=None): + async def _show_setup_form( + self, errors: dict[str, str] | None = None + ) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -39,7 +40,9 @@ class TwenteMilieuFlowHandler(ConfigFlow): errors=errors or {}, ) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) @@ -70,7 +73,7 @@ class TwenteMilieuFlowHandler(ConfigFlow): return self.async_abort(reason="already_configured") return self.async_create_entry( - title=unique_id, + title=str(unique_id), data={ CONF_ID: unique_id, CONF_POST_CODE: user_input[CONF_POST_CODE], diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 92194e12172..ad552a4b341 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -1,5 +1,7 @@ """Support for Twente Milieu sensors.""" -from typing import Any, Dict +from __future__ import annotations + +from typing import Any, Callable from twentemilieu import ( WASTE_TYPE_NON_RECYCLABLE, @@ -10,20 +12,23 @@ from twentemilieu import ( TwenteMilieuConnectionError, ) -from homeassistant.components.twentemilieu.const import DATA_UPDATE, DOMAIN +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType + +from .const import DATA_UPDATE, DOMAIN PARALLEL_UPDATES = 1 async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Twente Milieu sensor based on a config entry.""" twentemilieu = hass.data[DOMAIN][entry.data[CONF_ID]] @@ -67,7 +72,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class TwenteMilieuSensor(Entity): +class TwenteMilieuSensor(SensorEntity): """Defines a Twente Milieu sensor.""" def __init__( @@ -142,7 +147,7 @@ class TwenteMilieuSensor(Entity): self._state = next_pickup.date().isoformat() @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about Twente Milieu.""" return { "identifiers": {(DOMAIN, self._unique_id)}, diff --git a/homeassistant/components/twentemilieu/translations/hu.json b/homeassistant/components/twentemilieu/translations/hu.json index 8f88f82f2e5..df83a29ec22 100644 --- a/homeassistant/components/twentemilieu/translations/hu.json +++ b/homeassistant/components/twentemilieu/translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/twentemilieu/translations/id.json b/homeassistant/components/twentemilieu/translations/id.json new file mode 100644 index 00000000000..38746dfd12f --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_address": "Alamat tidak ditemukan di area layanan Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Abjad rumah/tambahan", + "house_number": "Nomor rumah", + "post_code": "Kode pos" + }, + "description": "Siapkan Twente Milieu untuk memberikan informasi pengumpulan sampah di alamat Anda.", + "title": "Twente Milieu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/ko.json b/homeassistant/components/twentemilieu/translations/ko.json index e6c19d40d06..a27df565f1b 100644 --- a/homeassistant/components/twentemilieu/translations/ko.json +++ b/homeassistant/components/twentemilieu/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "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", diff --git a/homeassistant/components/twilio/translations/de.json b/homeassistant/components/twilio/translations/de.json index 61df22c10f8..d9f071e8ff7 100644 --- a/homeassistant/components/twilio/translations/de.json +++ b/homeassistant/components/twilio/translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chtest du mit der Einrichtung beginnen?", + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", "title": "Twilio-Webhook einrichten" } } diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json index 913e3d2a7a2..cd60890dab3 100644 --- a/homeassistant/components/twilio/translations/hu.json +++ b/homeassistant/components/twilio/translations/hu.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val] ( {twilio_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Twilio-t?", + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "A Twilio Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/twilio/translations/id.json b/homeassistant/components/twilio/translations/id.json new file mode 100644 index 00000000000..be16b1d4802 --- /dev/null +++ b/homeassistant/components/twilio/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." + }, + "create_entry": { + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [Webhooks dengan Twilio]({twilio_url}).\n\nIsikan info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content-Type: application/x-www-form-urlencoded\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?", + "title": "Siapkan Twilio Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/ko.json b/homeassistant/components/twilio/translations/ko.json index 72165dfb798..aeb7c09474d 100644 --- a/homeassistant/components/twilio/translations/ko.json +++ b/homeassistant/components/twilio/translations/ko.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\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.", + "webhook_not_internet_accessible": "\uc6f9 \ud6c5 \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc811\uadfc\ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." }, "create_entry": { - "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio \uc6f9 \ud6c5]({twilio_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "Home Assistant\ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Twilio \uc6f9 \ud6c5]({twilio_url})\uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694:\n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/x-www-form-urlencoded\n \nHome Assistant\ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\uad00\ub828 \ubb38\uc11c]({docs_url})\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/twilio/translations/nl.json b/homeassistant/components/twilio/translations/nl.json index 55db4ef5e48..0d5d33a727e 100644 --- a/homeassistant/components/twilio/translations/nl.json +++ b/homeassistant/components/twilio/translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Weet u zeker dat u Twilio wilt instellen?", + "description": "Wil je beginnen met instellen?", "title": "Stel de Twilio Webhook in" } } diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py index f1593de5643..0a9adf76e0e 100644 --- a/homeassistant/components/twinkly/config_flow.py +++ b/homeassistant/components/twinkly/config_flow.py @@ -18,11 +18,9 @@ from .const import ( DEV_ID, DEV_MODEL, DEV_NAME, + DOMAIN, ) -# https://github.com/PyCQA/pylint/issues/3202 -from .const import DOMAIN # pylint: disable=unused-import - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 8de51d19d51..4353aa2707b 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -1,8 +1,9 @@ """The Twinkly light component.""" +from __future__ import annotations import asyncio import logging -from typing import Any, Dict, Optional +from typing import Any from aiohttp import ClientError @@ -84,7 +85,7 @@ class TwinklyLight(LightEntity): return self._is_available @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Id of the device.""" return self._id @@ -104,7 +105,7 @@ class TwinklyLight(LightEntity): return "mdi:string-lights" @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> dict[str, Any] | None: """Get device specific attributes.""" return ( { @@ -123,12 +124,12 @@ class TwinklyLight(LightEntity): return self._is_on @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of the light.""" return self._brightness @property - def state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return device specific state attributes.""" attributes = self._attributes diff --git a/homeassistant/components/twinkly/translations/hu.json b/homeassistant/components/twinkly/translations/hu.json new file mode 100644 index 00000000000..190d7e469d5 --- /dev/null +++ b/homeassistant/components/twinkly/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "device_exists": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/id.json b/homeassistant/components/twinkly/translations/id.json new file mode 100644 index 00000000000..b4a5ba6cbfa --- /dev/null +++ b/homeassistant/components/twinkly/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host (atau alamat IP) perangkat twinkly Anda" + }, + "description": "Siapkan string led Twinkly Anda", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/ko.json b/homeassistant/components/twinkly/translations/ko.json index 207037cba60..b3b23991e3d 100644 --- a/homeassistant/components/twinkly/translations/ko.json +++ b/homeassistant/components/twinkly/translations/ko.json @@ -5,6 +5,15 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "Twinkly \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 (\ub610\ub294 IP \uc8fc\uc18c)" + }, + "description": "Twinkly LED \uc904 \uc870\uba85 \uc124\uc815\ud558\uae30", + "title": "Twinkly" + } } } } \ No newline at end of file diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 4b019628158..cfabcf1045f 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -5,10 +5,9 @@ from requests.exceptions import HTTPError from twitch import TwitchClient import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True) -class TwitchSensor(Entity): +class TwitchSensor(SensorEntity): """Representation of an Twitch channel.""" def __init__(self, channel, client): @@ -88,7 +87,7 @@ class TwitchSensor(Entity): return self._preview @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attr = dict(self._statistics) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 873c9e12ab1..297f990e9df 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,6 +2,6 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.6.6"], + "requirements": ["TwitterAPI==2.6.8"], "codeowners": [] } diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 62e8fc17dff..ac7de89a61b 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -201,7 +201,7 @@ class TwitterNotificationService(BaseNotificationService): method_override="GET", ) if resp.status_code != HTTP_OK: - _LOGGER.error("media processing error: %s", resp.json()) + _LOGGER.error("Media processing error: %s", resp.json()) processing_info = resp.json()["processing_info"] _LOGGER.debug("media processing %s status: %s", media_id, processing_info) diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index e7c01479a96..f5cb21edcf7 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -6,10 +6,9 @@ import re import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_MODE, HTTP_OK, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -32,9 +31,7 @@ CONF_DESTINATION = "destination" _QUERY_SCHEME = vol.Schema( { - vol.Required(CONF_MODE): vol.All( - cv.ensure_list, [vol.In(list(["bus", "train"]))] - ), + vol.Required(CONF_MODE): vol.All(cv.ensure_list, [vol.In(["bus", "train"])]), vol.Required(CONF_ORIGIN): cv.string, vol.Required(CONF_DESTINATION): cv.string, } @@ -85,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class UkTransportSensor(Entity): +class UkTransportSensor(SensorEntity): """ Sensor that reads the UK transport web API. @@ -191,7 +188,7 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): self._state = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" attrs = {} if self._data is not None: @@ -261,7 +258,7 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor): self._state = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" attrs = {} if self._data is not None: diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 5a0a4969f09..094bae05881 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -123,8 +123,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_site() - host = self.config.get(CONF_HOST) - if not host and await async_discover_unifi(self.hass): + if not (host := self.config.get(CONF_HOST, "")) and await async_discover_unifi( + self.hass + ): host = "unifi" data = self.reauth_schema or { @@ -318,7 +319,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): if "name" in wlan } ) - ssid_filter = {ssid: ssid for ssid in sorted(list(ssids))} + ssid_filter = {ssid: ssid for ssid in sorted(ssids)} return self.async_show_form( step_id="device_tracker", diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 128f0107984..c77987bcbdd 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,8 +1,9 @@ """UniFi Controller abstraction.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta import ssl -from typing import Optional from aiohttp import CookieJar import aiounifi @@ -28,6 +29,7 @@ import async_timeout from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, @@ -40,6 +42,7 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util @@ -73,7 +76,7 @@ from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) -SUPPORTED_PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] CLIENT_CONNECTED = ( WIRED_CLIENT_CONNECTED, @@ -338,21 +341,21 @@ class UniFiController: self._site_role = description[0]["site_role"] - # Restore clients that is not a part of active clients list. + # Restore clients that are not a part of active clients list. entity_registry = await self.hass.helpers.entity_registry.async_get_registry() - for entity in entity_registry.entities.values(): - if ( - entity.config_entry_id != self.config_entry.entry_id - or "-" not in entity.unique_id + for entry in async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ): + if entry.domain == TRACKER_DOMAIN: + mac = entry.unique_id.split("-", 1)[0] + elif entry.domain == SWITCH_DOMAIN and ( + entry.unique_id.startswith(BLOCK_SWITCH) + or entry.unique_id.startswith(POE_SWITCH) ): + mac = entry.unique_id.split("-", 1)[1] + else: continue - mac = "" - if entity.domain == TRACKER_DOMAIN: - mac = entity.unique_id.split("-", 1)[0] - elif entity.domain == SWITCH_DOMAIN: - mac = entity.unique_id.split("-", 1)[1] - if mac in self.api.clients or mac not in self.api.clients_all: continue @@ -360,7 +363,7 @@ class UniFiController: self.api.clients.process_raw([client.raw]) LOGGER.debug( "Restore disconnected client %s (%s)", - entity.entity_id, + entry.entity_id, client.mac, ) @@ -368,7 +371,7 @@ class UniFiController: self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() - for platform in SUPPORTED_PLATFORMS: + for platform in PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -387,7 +390,7 @@ class UniFiController: @callback def async_heartbeat( - self, unique_id: str, heartbeat_expire_time: Optional[datetime] = None + self, unique_id: str, heartbeat_expire_time: datetime | None = None ) -> None: """Signal when a device has fresh home state.""" if heartbeat_expire_time is not None: @@ -415,9 +418,8 @@ class UniFiController: If config entry is updated due to reauth flow the entry might already have been reset and thus is not available. """ - if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]: + if not (controller := hass.data[UNIFI_DOMAIN].get(config_entry.entry_id)): return - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.load_config_entry_options() async_dispatcher_send(hass, controller.signal_options_update) @@ -465,7 +467,7 @@ class UniFiController: self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform ) - for platform in SUPPORTED_PLATFORMS + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ac28f7475f6..9842184e2ee 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -205,10 +205,13 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): elif not self.heartbeat_check: self.schedule_update = True - elif not self.client.event and self.client.last_updated == SOURCE_DATA: - if self.is_wired == self.client.is_wired: - self._is_connected = True - self.schedule_update = True + elif ( + not self.client.event + and self.client.last_updated == SOURCE_DATA + and self.is_wired == self.client.is_wired + ): + self._is_connected = True + self.schedule_update = True if self.schedule_update: self.schedule_update = False @@ -249,7 +252,7 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): return f"{self.client.mac}-{self.controller.site}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the client state attributes.""" raw = self.client.raw @@ -421,7 +424,7 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" if self.device.state == 0: return {} diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index f78ec614da1..755d95a061b 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -3,7 +3,10 @@ Support for bandwidth sensors of network clients. Support for uptime sensors of network clients. """ -from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN + +from datetime import datetime, timedelta + +from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN, SensorEntity from homeassistant.const import DATA_MEGABYTES from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -76,7 +79,7 @@ def add_uptime_entities(controller, async_add_entities, clients): async_add_entities(sensors) -class UniFiBandwidthSensor(UniFiClient): +class UniFiBandwidthSensor(UniFiClient, SensorEntity): """UniFi bandwidth sensor base class.""" DOMAIN = DOMAIN @@ -123,7 +126,7 @@ class UniFiTxBandwidthSensor(UniFiBandwidthSensor): return self.client.tx_bytes / 1000000 -class UniFiUpTimeSensor(UniFiClient): +class UniFiUpTimeSensor(UniFiClient, SensorEntity): """UniFi uptime sensor.""" DOMAIN = DOMAIN @@ -140,8 +143,10 @@ class UniFiUpTimeSensor(UniFiClient): return f"{super().name} {self.TYPE.capitalize()}" @property - def state(self) -> int: + def state(self) -> datetime: """Return the uptime of the client.""" + if self.client.uptime < 1000000000: + return (dt_util.now() - timedelta(seconds=self.client.uptime)).isoformat() return dt_util.utc_from_timestamp(float(self.client.uptime)).isoformat() async def options_updated(self) -> None: diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index e596e0b1e2a..f04acaaec87 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -18,6 +18,7 @@ from aiounifi.events import ( from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.restore_state import RestoreEntity from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN @@ -50,19 +51,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return # Store previously known POE control entities in case their POE are turned off. - previously_known_poe_clients = [] + known_poe_clients = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() - for entity in entity_registry.entities.values(): + for entry in async_entries_for_config_entry(entity_registry, config_entry.entry_id): - if ( - entity.config_entry_id != config_entry.entry_id - or not entity.unique_id.startswith(POE_SWITCH) - ): + if not entry.unique_id.startswith(POE_SWITCH): continue - mac = entity.unique_id.replace(f"{POE_SWITCH}-", "") - if mac in controller.api.clients or mac in controller.api.clients_all: - previously_known_poe_clients.append(entity.unique_id) + mac = entry.unique_id.replace(f"{POE_SWITCH}-", "") + if mac not in controller.api.clients: + continue + + known_poe_clients.append(mac) for mac in controller.option_block_clients: if mac not in controller.api.clients and mac in controller.api.clients_all: @@ -80,9 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): add_block_entities(controller, async_add_entities, clients) if controller.option_poe_clients: - add_poe_entities( - controller, async_add_entities, clients, previously_known_poe_clients - ) + add_poe_entities(controller, async_add_entities, clients, known_poe_clients) if controller.option_dpi_restrictions: add_dpi_entities(controller, async_add_entities, dpi_groups) @@ -91,7 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) items_added() - previously_known_poe_clients.clear() + known_poe_clients.clear() @callback @@ -111,9 +109,7 @@ def add_block_entities(controller, async_add_entities, clients): @callback -def add_poe_entities( - controller, async_add_entities, clients, previously_known_poe_clients -): +def add_poe_entities(controller, async_add_entities, clients, known_poe_clients): """Add new switch entities from the controller.""" switches = [] @@ -123,10 +119,13 @@ def add_poe_entities( if mac in controller.entities[DOMAIN][POE_SWITCH]: continue - poe_client_id = f"{POE_SWITCH}-{mac}" client = controller.api.clients[mac] - if poe_client_id not in previously_known_poe_clients and ( + # Try to identify new clients powered by POE. + # Known POE clients have been created in previous HASS sessions. + # If port_poe is None the port does not support POE + # If poe_enable is False we can't know if a POE client is available for control. + if mac not in known_poe_clients and ( mac in controller.wireless_clients or client.sw_mac not in devices or not devices[client.sw_mac].ports[client.sw_port].port_poe @@ -139,7 +138,7 @@ def add_poe_entities( multi_clients_on_port = False for client2 in controller.api.clients.values(): - if poe_client_id in previously_known_poe_clients: + if mac in known_poe_clients: break if ( @@ -196,18 +195,19 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() - state = await self.async_get_last_state() - if state is None: + if self.poe_mode: # POE is enabled and client in a known state return - if self.poe_mode is None: - self.poe_mode = state.attributes["poe_mode"] + if (state := await self.async_get_last_state()) is None: + return + + self.poe_mode = state.attributes.get("poe_mode") if not self.client.sw_mac: - self.client.raw["sw_mac"] = state.attributes["switch"] + self.client.raw["sw_mac"] = state.attributes.get("switch") if not self.client.sw_port: - self.client.raw["sw_port"] = state.attributes["port"] + self.client.raw["sw_port"] = state.attributes.get("port") @property def is_on(self): @@ -218,16 +218,15 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): def available(self): """Return if switch is available. - Poe_mode None means its poe state is unknown. + Poe_mode None means its POE state is unknown. Sw_mac unavailable means restored client. """ return ( - self.poe_mode is None - or self.client.sw_mac - and ( - self.controller.available - and self.client.sw_mac in self.controller.api.devices - ) + self.poe_mode is not None + and self.controller.available + and self.client.sw_port + and self.client.sw_mac + and self.client.sw_mac in self.controller.api.devices ) async def async_turn_on(self, **kwargs): @@ -239,7 +238,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): await self.device.async_set_port_poe_mode(self.client.sw_port, "off") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = { "power": self.port.poe_power, @@ -257,15 +256,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): @property def port(self): """Shortcut to the switch port that client is connected to.""" - try: - return self.device.ports[self.client.sw_port] - except (AttributeError, KeyError, TypeError): - _LOGGER.warning( - "Entity %s reports faulty device %s or port %s", - self.entity_id, - self.client.sw_mac, - self.client.sw_port, - ) + return self.device.ports[self.client.sw_port] async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" @@ -288,10 +279,11 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): @callback def async_update_callback(self) -> None: """Update the clients state.""" - if self.client.last_updated == SOURCE_EVENT: - - if self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED: - self._is_blocked = self.client.event.event in CLIENT_BLOCKED + if ( + self.client.last_updated == SOURCE_EVENT + and self.client.event.event in CLIENT_BLOCKED + CLIENT_UNBLOCKED + ): + self._is_blocked = self.client.event.event in CLIENT_BLOCKED super().async_update_callback() diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 05dd66fe56c..b1d3e495f94 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -10,6 +10,7 @@ "service_unavailable": "Verbindung fehlgeschlagen", "unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar." }, + "flow_title": "UniFi Netzwerk {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/he.json b/homeassistant/components/unifi/translations/he.json new file mode 100644 index 00000000000..3007c0e968c --- /dev/null +++ b/homeassistant/components/unifi/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 2a7a43d42e9..4602193850f 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -1,9 +1,14 @@ { "config": { + "abort": { + "configuration_updated": "A konfigur\u00e1ci\u00f3 friss\u00edtve.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, "error": { "faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "service_unavailable": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { @@ -12,7 +17,7 @@ "port": "Port", "site": "Site azonos\u00edt\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "verify_ssl": "Vez\u00e9rl\u0151 megfelel\u0151 tan\u00fas\u00edtv\u00e1nnyal" + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, "title": "UniFi vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" } @@ -23,6 +28,9 @@ "client_control": { "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st." }, + "simple_options": { + "description": "UniFi integr\u00e1ci\u00f3 konfigur\u00e1l\u00e1sa" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" diff --git a/homeassistant/components/unifi/translations/id.json b/homeassistant/components/unifi/translations/id.json new file mode 100644 index 00000000000..7a707b28aa0 --- /dev/null +++ b/homeassistant/components/unifi/translations/id.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "Situs Controller sudah dikonfigurasi", + "configuration_updated": "Konfigurasi diperbarui.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "faulty_credentials": "Autentikasi tidak valid", + "service_unavailable": "Gagal terhubung", + "unknown_client_mac": "Tidak ada klien yang tersedia di alamat MAC tersebut" + }, + "flow_title": "UniFi Network {site} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "site": "ID Site", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "title": "Siapkan UniFi Controller" + } + } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "Klien yang dikontrol akses jaringan", + "dpi_restrictions": "Izinkan kontrol grup pembatasan DPI", + "poe_clients": "Izinkan kontrol POE klien" + }, + "description": "Konfigurasikan kontrol klien \n\nBuat sakelar untuk nomor seri yang ingin dikontrol akses jaringannya.", + "title": "Opsi UniFi 2/3" + }, + "device_tracker": { + "data": { + "detection_time": "Tenggang waktu dalam detik dari terakhir terlihat hingga dianggap sebagai keluar", + "ignore_wired_bug": "Nonaktifkan bug logika kabel UniFi", + "ssid_filter": "Pilih SSID untuk melacak klien nirkabel", + "track_clients": "Lacak klien jaringan", + "track_devices": "Lacak perangkat jaringan (perangkat Ubiquiti)", + "track_wired_clients": "Sertakan klien jaringan berkabel" + }, + "description": "Konfigurasikan pelacakan perangkat", + "title": "Opsi UniFi 1/3" + }, + "simple_options": { + "data": { + "block_client": "Klien yang dikontrol akses jaringan", + "track_clients": "Lacak klien jaringan", + "track_devices": "Lacak perangkat jaringan (perangkat Ubiquiti)" + }, + "description": "Konfigurasikan integrasi UniFi" + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Sensor penggunaan bandwidth untuk klien jaringan", + "allow_uptime_sensors": "Sensor waktu kerja untuk klien jaringan" + }, + "description": "Konfigurasikan sensor statistik", + "title": "Opsi UniFi 3/3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json index 454feec0922..3a13b420097 100644 --- a/homeassistant/components/unifi/translations/ko.json +++ b/homeassistant/components/unifi/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "configuration_updated": "\uad6c\uc131\uc774 \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -9,6 +10,7 @@ "service_unavailable": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "unknown_client_mac": "\ud574\ub2f9 MAC \uc8fc\uc18c\uc5d0\uc11c \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, + "flow_title": "UniFi \ub124\ud2b8\uc6cc\ud06c: {site} ({host})", "step": { "user": { "data": { @@ -27,10 +29,11 @@ "step": { "client_control": { "data": { - "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", - "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9" + "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc811\uadfc \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", + "dpi_restrictions": "DPI \uc81c\ud55c \uadf8\ub8f9\uc758 \uc81c\uc5b4 \ud5c8\uc6a9\ud558\uae30", + "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9\ud558\uae30" }, - "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4\ub97c \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.", + "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc811\uadfc\uc744 \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.", "title": "UniFi \uc635\uc158 2/3" }, "device_tracker": { @@ -47,7 +50,7 @@ }, "simple_options": { "data": { - "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", + "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc811\uadfc \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1", "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)" }, @@ -56,7 +59,7 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c", - "allow_uptime_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \uac00\ub3d9 \uc2dc\uac04 \uc13c\uc11c" + "allow_uptime_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub300\ud55c \uac00\ub3d9 \uc2dc\uac04 \uc13c\uc11c" }, "description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131", "title": "UniFi \uc635\uc158 3/3" diff --git a/homeassistant/components/unifi/translations/lb.json b/homeassistant/components/unifi/translations/lb.json index 1e4870e6ab8..5c939a0105a 100644 --- a/homeassistant/components/unifi/translations/lb.json +++ b/homeassistant/components/unifi/translations/lb.json @@ -8,6 +8,7 @@ "service_unavailable": "Feeler beim verbannen", "unknown_client_mac": "Kee Cliwent mat der MAC Adress disponibel" }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 7f0baf4a3be..e5e5e3a1dfb 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -6,8 +6,8 @@ "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "faulty_credentials": "Foutieve gebruikersgegevens", - "service_unavailable": "Geen service beschikbaar", + "faulty_credentials": "Ongeldige authenticatie", + "service_unavailable": "Kan geen verbinding maken", "unknown_client_mac": "Geen client beschikbaar op dat MAC-adres" }, "flow_title": "UniFi Netwerk {site} ({host})", @@ -19,7 +19,7 @@ "port": "Poort", "site": "Site ID", "username": "Gebruikersnaam", - "verify_ssl": "Controller gebruik van het juiste certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "title": "Stel de UniFi-controller in" } @@ -30,6 +30,7 @@ "client_control": { "data": { "block_client": "Cli\u00ebnten met netwerktoegang", + "dpi_restrictions": "Sta controle van DPI-beperkingsgroepen toe", "poe_clients": "Sta POE-controle van gebruikers toe" }, "description": "Configureer clientbesturingen \n\n Maak schakelaars voor serienummers waarvoor u de netwerktoegang wilt beheren.", diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 5810204db41..769287bb975 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -18,7 +18,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "site": "ID \u0441\u0430\u0439\u0442\u0430", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "title": "UniFi Controller" diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 1834d22855c..f6e770c126f 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -1,6 +1,7 @@ """Combination of multiple media players for a universal controller.""" +from __future__ import annotations + from copy import copy -from typing import Optional import voluptuous as vol @@ -270,7 +271,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): return False @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device.""" return self._device_class @@ -470,7 +471,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_MEDIA_PREVIOUS_TRACK in self._cmds: flags |= SUPPORT_PREVIOUS_TRACK - if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]): + if any(cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]): flags |= SUPPORT_VOLUME_STEP if SERVICE_VOLUME_SET in self._cmds: flags |= SUPPORT_VOLUME_SET @@ -502,7 +503,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): return flags @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" active_child = self._child_state return {ATTR_ACTIVE_CHILD: active_child.entity_id} if active_child else {} diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index f2765ff317d..ba9faeb1797 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -3,26 +3,19 @@ import asyncio import upb_lib -from homeassistant.const import CONF_FILE_PATH, CONF_HOST -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ATTR_COMMAND, CONF_FILE_PATH, CONF_HOST +from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ADDRESS, ATTR_BRIGHTNESS_PCT, - ATTR_COMMAND, ATTR_RATE, DOMAIN, EVENT_UPB_SCENE_CHANGED, ) -UPB_PLATFORMS = ["light", "scene"] - - -async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: - """Set up the UPB platform.""" - return True +PLATFORMS = ["light", "scene"] async def async_setup_entry(hass, config_entry): @@ -36,9 +29,9 @@ async def async_setup_entry(hass, config_entry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} - for component in UPB_PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) def _element_changed(element, changeset): @@ -71,8 +64,8 @@ async def async_unload_entry(hass, config_entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in UPB_PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) @@ -111,7 +104,7 @@ class UpbEntity(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the default attributes of the element.""" return self._element.as_dict() diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 3af9999bd9f..c1fa31ea467 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -1,5 +1,6 @@ """Config flow for UPB PIM integration.""" import asyncio +from contextlib import suppress import logging from urllib.parse import urlparse @@ -10,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_ADDRESS, CONF_FILE_PATH, CONF_HOST, CONF_PROTOCOL -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) PROTOCOL_MAP = {"TCP": "tcp://", "Serial port": "serial://"} @@ -43,11 +44,8 @@ async def _validate_input(data): upb.connect(_connected_callback) - try: - with async_timeout.timeout(VALIDATE_TIMEOUT): - await connected_event.wait() - except asyncio.TimeoutError: - pass + with suppress(asyncio.TimeoutError), async_timeout.timeout(VALIDATE_TIMEOUT): + await connected_event.wait() upb.disconnect() diff --git a/homeassistant/components/upb/const.py b/homeassistant/components/upb/const.py index 75d754087e4..8a2c435a70f 100644 --- a/homeassistant/components/upb/const.py +++ b/homeassistant/components/upb/const.py @@ -10,7 +10,6 @@ ATTR_ADDRESS = "address" ATTR_BLINK_RATE = "blink_rate" ATTR_BRIGHTNESS = "brightness" ATTR_BRIGHTNESS_PCT = "brightness_pct" -ATTR_COMMAND = "command" ATTR_RATE = "rate" CONF_NETWORK = "network" EVENT_UPB_SCENE_CHANGED = "upb.scene_changed" diff --git a/homeassistant/components/upb/translations/hu.json b/homeassistant/components/upb/translations/hu.json new file mode 100644 index 00000000000..b09f497a0e4 --- /dev/null +++ b/homeassistant/components/upb/translations/hu.json @@ -0,0 +1,18 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/id.json b/homeassistant/components/upb/translations/id.json new file mode 100644 index 00000000000..3e875de7b19 --- /dev/null +++ b/homeassistant/components/upb/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_upb_file": "File ekspor UPB UPStart tidak ada atau tidak valid, periksa nama dan jalur berkas.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "address": "Alamat (lihat deskripsi di atas)", + "file_path": "Jalur dan nama file ekspor UPStart UPB.", + "protocol": "Protokol" + }, + "description": "Hubungkan Universal Powerline Bus Powerline Interface Module (UPB PIM). String alamat harus dalam format 'address[:port]' untuk 'tcp'. Nilai port ini opsional dan nilai bakunya adalah 2101. Contoh: '192.168.1.42'. Untuk protokol serial, alamat harus dalam format 'tty[:baud]'. Nilai baud ini opsional dan nilai bakunya adalah 4800. Contoh: '/dev/ttyS1'.", + "title": "Hubungkan ke UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/ko.json b/homeassistant/components/upb/translations/ko.json index da357f7a136..aedb6e06c41 100644 --- a/homeassistant/components/upb/translations/ko.json +++ b/homeassistant/components/upb/translations/ko.json @@ -15,8 +15,8 @@ "file_path": "UPStart UPB \ub0b4\ubcf4\ub0b4\uae30 \ud30c\uc77c\uc758 \uacbd\ub85c \ubc0f \uc774\ub984", "protocol": "\ud504\ub85c\ud1a0\ucf5c" }, - "description": "\ubc94\uc6a9 \ud30c\uc6cc\ub77c\uc778 \ubc84\uc2a4 \ud30c\uc6cc\ub77c\uc778 \uc778\ud130\ud398\uc774\uc2a4 \ubaa8\ub4c8 (UPB PIM) \uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694. \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tcp' \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 2101 \uc785\ub2c8\ub2e4. \uc608: '192.168.1.42'. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tty[:baud]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ubcf4(baud)\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 4800 \uc785\ub2c8\ub2e4. \uc608: '/dev/ttyS1'.", - "title": "UPB PIM \uc5d0 \uc5f0\uacb0\ud558\uae30" + "description": "\ubc94\uc6a9 \ud30c\uc6cc\ub77c\uc778 \ubc84\uc2a4 \ud30c\uc6cc\ub77c\uc778 \uc778\ud130\ud398\uc774\uc2a4 \ubaa8\ub4c8 (UPB PIM) \uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694. \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tcp' \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 2101 \uc785\ub2c8\ub2e4. \uc608: '192.168.1.42'. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 'tty[:baud]' \ud615\uc2dd\uc785\ub2c8\ub2e4. \uc804\uc1a1 \uc18d\ub3c4\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 4800 \uc785\ub2c8\ub2e4. \uc608: '/dev/ttyS1'.", + "title": "UPB PIM\uc5d0 \uc5f0\uacb0\ud558\uae30" } } } diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json index ae100e45845..657ce03e544 100644 --- a/homeassistant/components/upb/translations/pt.json +++ b/homeassistant/components/upb/translations/pt.json @@ -4,6 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index d68311a8793..2a2a1b3798b 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -1,6 +1,7 @@ """Support for UPC ConnectBox router.""" +from __future__ import annotations + import logging -from typing import List, Optional from connect_box import ConnectBox from connect_box.exceptions import ConnectBoxError, ConnectBoxLoginError @@ -57,7 +58,7 @@ class UPCDeviceScanner(DeviceScanner): """Initialize the scanner.""" self.connect_box: ConnectBox = connect_box - async def async_scan_devices(self) -> List[str]: + async def async_scan_devices(self) -> list[str]: """Scan for new devices and return a list with found device IDs.""" try: await self.connect_box.async_get_devices() @@ -66,7 +67,7 @@ class UPCDeviceScanner(DeviceScanner): return [device.mac for device in self.connect_box.devices] - async def async_get_device_name(self, device: str) -> Optional[str]: + async def async_get_device_name(self, device: str) -> str | None: """Get the device name (the name of the wireless device not used).""" for connected_device in self.connect_box.devices: if ( diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 5acf9e364bc..c118f12954d 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -1,9 +1,10 @@ """Support for UpCloud.""" +from __future__ import annotations import dataclasses from datetime import timedelta import logging -from typing import Dict, List +from typing import Dict import requests.exceptions import upcloud_api @@ -40,7 +41,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_CORE_NUMBER = "core_number" ATTR_HOSTNAME = "hostname" ATTR_MEMORY_AMOUNT = "memory_amount" -ATTR_STATE = "state" ATTR_TITLE = "title" ATTR_UUID = "uuid" ATTR_ZONE = "zone" @@ -92,7 +92,7 @@ class UpCloudDataUpdateCoordinator( hass, _LOGGER, name=f"{username}@UpCloud", update_interval=update_interval ) self.cloud_manager = cloud_manager - self.unsub_handlers: List[CALLBACK_TYPE] = [] + self.unsub_handlers: list[CALLBACK_TYPE] = [] async def async_update_config(self, config_entry: ConfigEntry) -> None: """Handle config update.""" @@ -100,7 +100,7 @@ class UpCloudDataUpdateCoordinator( seconds=config_entry.options[CONF_SCAN_INTERVAL] ) - async def _async_update_data(self) -> Dict[str, upcloud_api.Server]: + async def _async_update_data(self) -> dict[str, upcloud_api.Server]: return { x.uuid: x for x in await self.hass.async_add_executor_job( @@ -113,10 +113,10 @@ class UpCloudDataUpdateCoordinator( class UpCloudHassData: """Home Assistant UpCloud runtime data.""" - coordinators: Dict[str, UpCloudDataUpdateCoordinator] = dataclasses.field( + coordinators: dict[str, UpCloudDataUpdateCoordinator] = dataclasses.field( default_factory=dict ) - scan_interval_migrations: Dict[str, int] = dataclasses.field(default_factory=dict) + scan_interval_migrations: dict[str, int] = dataclasses.field(default_factory=dict) async def async_setup(hass: HomeAssistantType, config) -> bool: @@ -127,7 +127,7 @@ async def async_setup(hass: HomeAssistantType, config) -> bool: _LOGGER.warning( "Loading upcloud via top level config is deprecated and no longer " - "necessary as of 0.117. Please remove it from your YAML configuration." + "necessary as of 0.117; Please remove it from your YAML configuration" ) hass.async_create_task( hass.config_entries.flow.async_init( @@ -207,9 +207,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ) # Call the UpCloud API to refresh data - await coordinator.async_request_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() # Listen to config entry updates coordinator.unsub_handlers.append( @@ -297,7 +295,7 @@ class UpCloudServerEntity(CoordinatorEntity): return DEFAULT_COMPONENT_DEVICE_CLASS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the UpCloud server.""" return { x: getattr(self._server, x, None) diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index d7f1680c80f..1a39b189897 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -10,7 +10,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -# pylint: disable=unused-import # for DOMAIN https://github.com/PyCQA/pylint/issues/3202 from .const import DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 3a85f847c9d..f161e273bc3 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -3,6 +3,6 @@ "name": "UpCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upcloud", - "requirements": ["upcloud-api==0.4.5"], + "requirements": ["upcloud-api==1.0.1"], "codeowners": ["@scop"] } diff --git a/homeassistant/components/upcloud/translations/hu.json b/homeassistant/components/upcloud/translations/hu.json index 7a7de0633a7..2bb28c6c3bc 100644 --- a/homeassistant/components/upcloud/translations/hu.json +++ b/homeassistant/components/upcloud/translations/hu.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "user": { diff --git a/homeassistant/components/upcloud/translations/id.json b/homeassistant/components/upcloud/translations/id.json new file mode 100644 index 00000000000..4ff6a8c7d92 --- /dev/null +++ b/homeassistant/components/upcloud/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pembaruan (dalam detik, minimal 30)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/ko.json b/homeassistant/components/upcloud/translations/ko.json index 04360a9a8f7..24b4ab0a446 100644 --- a/homeassistant/components/upcloud/translations/ko.json +++ b/homeassistant/components/upcloud/translations/ko.json @@ -12,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc19f\uac12 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/nl.json b/homeassistant/components/upcloud/translations/nl.json index 312b117208c..f9ba7d2f696 100644 --- a/homeassistant/components/upcloud/translations/nl.json +++ b/homeassistant/components/upcloud/translations/nl.json @@ -12,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update-interval in seconden, minimaal 30" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/ru.json b/homeassistant/components/upcloud/translations/ru.json index c64a69965b2..8b0377e9c34 100644 --- a/homeassistant/components/upcloud/translations/ru.json +++ b/homeassistant/components/upcloud/translations/ru.json @@ -8,7 +8,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" } } } diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 9d65bb4c5d4..9e3c504e4be 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -5,7 +5,6 @@ import logging import async_timeout from awesomeversion import AwesomeVersion -from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol from homeassistant.const import __version__ as current_version @@ -23,20 +22,22 @@ CONF_COMPONENT_REPORTING = "include_used_components" DOMAIN = "updater" -UPDATER_URL = "https://updater.home-assistant.io/" +UPDATER_URL = "https://www.home-assistant.io/version.json" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { - vol.Optional(CONF_REPORTING, default=True): cv.boolean, - vol.Optional(CONF_COMPONENT_REPORTING, default=False): cv.boolean, + vol.Optional(CONF_REPORTING): cv.boolean, + vol.Optional(CONF_COMPONENT_REPORTING): cv.boolean, } }, extra=vol.ALLOW_EXTRA, ) RESPONSE_SCHEMA = vol.Schema( - {vol.Required("version"): cv.string, vol.Required("release-notes"): cv.url} + {vol.Required("current_version"): cv.string, vol.Required("release_notes"): cv.url}, + extra=vol.REMOVE_EXTRA, ) @@ -52,30 +53,27 @@ class Updater: async def async_setup(hass, config): """Set up the updater component.""" - if "dev" in current_version: - # This component only makes sense in release versions - _LOGGER.info("Running on 'dev', only analytics will be submitted") - conf = config.get(DOMAIN, {}) - if conf.get(CONF_REPORTING): - huuid = await hass.helpers.instance_id.async_get() - else: - huuid = None - include_components = conf.get(CONF_COMPONENT_REPORTING) + for option in (CONF_COMPONENT_REPORTING, CONF_REPORTING): + if option in conf: + _LOGGER.warning( + "Analytics reporting with the option '%s' " + "is deprecated and you should remove that from your configuration. " + "The analytics part of this integration has moved to the new 'analytics' integration", + option, + ) async def check_new_version() -> Updater: """Check if a new version is available and report if one is.""" - newest, release_notes = await get_newest_version( - hass, huuid, include_components - ) - - _LOGGER.debug("Fetched version %s: %s", newest, release_notes) - # Skip on dev if "dev" in current_version: return Updater(False, "", "") + newest, release_notes = await get_newest_version(hass) + + _LOGGER.debug("Fetched version %s: %s", newest, release_notes) + # Load data from Supervisor if hass.components.hassio.is_hassio(): core_info = hass.components.hassio.get_core_info() @@ -121,34 +119,12 @@ async def async_setup(hass, config): return True -async def get_newest_version(hass, huuid, include_components): +async def get_newest_version(hass): """Get the newest Home Assistant version.""" - if huuid: - info_object = await hass.helpers.system_info.async_get_system_info() - - if include_components: - info_object["components"] = list(hass.config.components) - - linux_dist = await hass.async_add_executor_job(linux_distribution, False) - info_object["distribution"] = linux_dist[0] - info_object["os_version"] = linux_dist[1] - - info_object["huuid"] = huuid - else: - info_object = {} - session = async_get_clientsession(hass) with async_timeout.timeout(30): - req = await session.post(UPDATER_URL, json=info_object) - - _LOGGER.info( - ( - "Submitted analytics to Home Assistant servers. " - "Information submitted includes %s" - ), - info_object, - ) + req = await session.get(UPDATER_URL) try: res = await req.json() @@ -159,7 +135,7 @@ async def get_newest_version(hass, huuid, include_components): try: res = RESPONSE_SCHEMA(res) - return res["version"], res["release-notes"] + return res["current_version"], res["release_notes"] except vol.Invalid as err: raise update_coordinator.UpdateFailed( f"Got unexpected response: {err}" diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 36e05513d43..93d19029992 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -35,7 +35,7 @@ class UpdaterBinary(CoordinatorEntity, BinarySensorEntity): return self.coordinator.data.update_available @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the optional state attributes.""" if not self.coordinator.data: return None diff --git a/homeassistant/components/updater/translations/zh-Hant.json b/homeassistant/components/updater/translations/zh-Hant.json index 31188faa135..23c1b069fc1 100644 --- a/homeassistant/components/updater/translations/zh-Hant.json +++ b/homeassistant/components/updater/translations/zh-Hant.json @@ -1,3 +1,3 @@ { - "title": "\u66f4\u65b0\u7248\u672c" + "title": "\u66f4\u65b0\u5668" } \ No newline at end of file diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index e1101c3713c..1cbaf931857 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,6 +1,8 @@ """Config flow for UPNP.""" +from __future__ import annotations + from datetime import timedelta -from typing import Any, Mapping, Optional +from typing import Any, Mapping import voluptuous as vol @@ -55,7 +57,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discoveries: Mapping = None async def async_step_user( - self, user_input: Optional[Mapping] = None + self, user_input: Mapping | None = None ) -> Mapping[str, Any]: """Handle a flow start.""" _LOGGER.debug("async_step_user: user_input: %s", user_input) @@ -111,9 +113,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=data_schema, ) - async def async_step_import( - self, import_info: Optional[Mapping] - ) -> Mapping[str, Any]: + async def async_step_import(self, import_info: Mapping | None) -> Mapping[str, Any]: """Import a new UPnP/IGD device as a config entry. This flow is triggered by `async_setup`. If no device has been @@ -204,7 +204,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_ssdp_confirm() async def async_step_ssdp_confirm( - self, user_input: Optional[Mapping] = None + self, user_input: Mapping | None = None ) -> Mapping[str, Any]: """Confirm integration via SSDP.""" _LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index a06ca254c87..034496ec028 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from ipaddress import IPv4Address -from typing import List, Mapping +from typing import Mapping from urllib.parse import urlparse from async_upnp_client import UpnpFactory @@ -42,7 +42,7 @@ class Device: self._igd_device: IgdDevice = igd_device @classmethod - async def async_discover(cls, hass: HomeAssistantType) -> List[Mapping]: + async def async_discover(cls, hass: HomeAssistantType) -> list[Mapping]: """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = None diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index fe3a7b169dc..feecdb00b18 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.14.13"], + "requirements": ["async-upnp-client==0.16.0"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 97d3c1a702c..0e95b6106a3 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,7 +1,10 @@ """Support for UPnP/IGD Sensors.""" -from datetime import timedelta -from typing import Any, Mapping, Optional +from __future__ import annotations +from datetime import timedelta +from typing import Any, Mapping + +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND from homeassistant.helpers import device_registry as dr @@ -115,7 +118,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class UpnpSensor(CoordinatorEntity): +class UpnpSensor(CoordinatorEntity, SensorEntity): """Base class for UPnP/IGD sensors.""" def __init__( @@ -176,7 +179,7 @@ class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the device.""" device_value_key = self._sensor_type["device_value_key"] value = self.coordinator.data[device_value_key] @@ -214,7 +217,7 @@ class DerivedUpnpSensor(UpnpSensor): return current_value < self._last_value @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the device.""" # Can't calculate any derivative if we have only one value. device_value_key = self._sensor_type["device_value_key"] diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index 972f8da4075..8a5f4fefadf 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "scan_interval": "Aktualisierungsintervall (Sekunden, mindestens 30)", "usn": "Ger\u00e4t" } } diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 66320386a89..8b50de71f74 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Az UPnP / IGD m\u00e1r konfigur\u00e1l\u00e1sra ker\u00fclt", - "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { "one": "hiba", "other": "" + }, + "flow_title": "UPnP/IGD: {name}", + "step": { + "user": { + "data": { + "usn": "Eszk\u00f6z" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/id.json b/homeassistant/components/upnp/translations/id.json new file mode 100644 index 00000000000..463e61f271c --- /dev/null +++ b/homeassistant/components/upnp/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "incomplete_discovery": "Proses penemuan tidak selesai", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "UPnP/IGD: {name}", + "step": { + "ssdp_confirm": { + "description": "Ingin menyiapkan perangkat UPnP/IGD ini?" + }, + "user": { + "data": { + "scan_interval": "Interval pembaruan (dalam detik, minimal 30)", + "usn": "Perangkat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ko.json b/homeassistant/components/upnp/translations/ko.json index 7dd2e5c685c..ab80ceb9caa 100644 --- a/homeassistant/components/upnp/translations/ko.json +++ b/homeassistant/components/upnp/translations/ko.json @@ -12,7 +12,7 @@ }, "user": { "data": { - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc18c\uac12 30)", + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc19f\uac12 30)", "usn": "\uae30\uae30" } } diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index 3d2c628fcbb..331d5850fc4 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "incomplete_discovery": "Onvolledige ontdekking", - "no_devices_found": "Geen UPnP/IGD apparaten gevonden op het netwerk." + "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { "one": "Een", @@ -11,8 +11,12 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { + "init": { + "one": "Leeg", + "other": "Leeg" + }, "ssdp_confirm": { - "description": "Wilt u [%%] instellen?" + "description": "Wilt u dit UPnP/IGD-apparaat instellen?" }, "user": { "data": { diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 8363d2da2cb..7e79c2fbb5e 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -2,10 +2,13 @@ import voluptuous as vol -from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, PLATFORM_SCHEMA +from homeassistant.components.sensor import ( + DEVICE_CLASS_TIMESTAMP, + PLATFORM_SCHEMA, + SensorEntity, +) from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util DEFAULT_NAME = "Uptime" @@ -30,7 +33,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([UptimeSensor(name)], True) -class UptimeSensor(Entity): +class UptimeSensor(SensorEntity): """Representation of an uptime sensor.""" def __init__(self, name): diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index e31d8b44b10..6c0bb63c70f 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -75,7 +75,7 @@ class UptimeRobotBinarySensor(BinarySensorEntity): return DEVICE_CLASS_CONNECTIVITY @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the binary sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self._target} diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 2e15a4b1422..bd261aba4fb 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -5,10 +5,9 @@ import logging import uscisstatus import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -33,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Setup USCIS Sensor Fail check if your Case ID is Valid") -class UscisSensor(Entity): +class UscisSensor(SensorEntity): """USCIS Sensor will check case status on daily basis.""" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=24) @@ -60,7 +59,7 @@ class UscisSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 9fd98de42df..eefa2ed1d0d 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -1,7 +1,8 @@ """Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Optional from geojson_client.usgs_earthquake_hazards_program_feed import ( UsgsEarthquakeHazardsProgramFeedManager, @@ -255,22 +256,22 @@ class UsgsEarthquakesEvent(GeolocationEvent): return SOURCE @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def distance(self) -> Optional[float]: + def distance(self) -> float | None: """Return distance value of this external event.""" return self._distance @property - def latitude(self) -> Optional[float]: + def latitude(self) -> float | None: """Return latitude value of this external event.""" return self._latitude @property - def longitude(self) -> Optional[float]: + def longitude(self) -> float | None: """Return longitude value of this external event.""" return self._longitude @@ -280,7 +281,7 @@ class UsgsEarthquakesEvent(GeolocationEvent): return DEFAULT_UNIT_OF_MEASUREMENT @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" attributes = {} for key, value in ( diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 24bfd77f762..5442cd583e2 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -165,7 +165,7 @@ class TariffSelect(RestoreEntity): return self._current_tariff @property - def state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {ATTR_TARIFFS: self._tariffs} diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index e8d551ba280..d28819a38cb 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,6 +5,7 @@ import logging import voluptuous as vol +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -102,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class UtilityMeterSensor(RestoreEntity): +class UtilityMeterSensor(RestoreEntity, SensorEntity): """Representation of an utility meter sensor.""" def __init__( @@ -324,7 +325,7 @@ class UtilityMeterSensor(RestoreEntity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the sensor.""" state_attr = { ATTR_SOURCE_ID: self._sensor_source_id, diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index a20b99d673a..6bbd868a8bd 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -1,8 +1,9 @@ """Support for Ubiquiti's UVC cameras.""" +from __future__ import annotations + from datetime import datetime import logging import re -from typing import Optional import requests from uvcclient import camera as uvc_camera, nvr @@ -12,6 +13,7 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Cam from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.util.dt import utc_from_timestamp _LOGGER = logging.getLogger(__name__) @@ -111,7 +113,7 @@ class UnifiVideoCamera(Camera): return 0 @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the camera state attributes.""" attr = {} if self.motion_detection_enabled: @@ -127,11 +129,9 @@ class UnifiVideoCamera(Camera): if "recordingIndicator" in self._caminfo: recording_state = self._caminfo["recordingIndicator"] - return ( - self._caminfo["recordingSettings"]["fullTimeRecordEnabled"] - or recording_state == "MOTION_INPROGRESS" - or recording_state == "MOTION_FINISHED" - ) + return self._caminfo["recordingSettings"][ + "fullTimeRecordEnabled" + ] or recording_state in ["MOTION_INPROGRESS", "MOTION_FINISHED"] @property def motion_detection_enabled(self): @@ -196,9 +196,8 @@ class UnifiVideoCamera(Camera): def camera_image(self): """Return the image of this camera.""" - if not self._camera: - if not self._login(): - return + if not self._camera and not self._login(): + return def _get_image(retry=True): try: @@ -255,8 +254,8 @@ class UnifiVideoCamera(Camera): self._caminfo = self._nvr.get_camera(self._uuid) -def timestamp_ms_to_date(epoch_ms: int) -> Optional[datetime]: +def timestamp_ms_to_date(epoch_ms: int) -> datetime | None: """Convert millisecond timestamp to datetime.""" if epoch_ms: - return datetime.fromtimestamp(epoch_ms / 1000) + return utc_from_timestamp(epoch_ms / 1000) return None diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ca5caec7ac1..d8803931f38 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta from functools import partial import logging +from typing import final import voluptuous as vol @@ -271,6 +272,7 @@ class VacuumEntity(_BaseVacuum, ToggleEntity): battery_level=self.battery_level, charging=charging ) + @final @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index ed25289da10..2308882469e 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Vacuum.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -26,7 +26,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Vacuum devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -57,7 +57,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" config = ACTION_SCHEMA(config) diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index cb17505f6e1..4803ebdb988 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -1,5 +1,5 @@ """Provide the device automations for Vacuum.""" -from typing import Dict, List +from __future__ import annotations import voluptuous as vol @@ -30,7 +30,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( async def async_get_conditions( hass: HomeAssistant, device_id: str -) -> List[Dict[str, str]]: +) -> list[dict[str, str]]: """List device conditions for Vacuum devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 21a2ae5e8c2..d5c596b209a 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -1,5 +1,5 @@ """Provides device automations for Vacuum.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, + CONF_FOR, CONF_PLATFORM, CONF_TYPE, ) @@ -25,11 +26,12 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: """List device triggers for Vacuum devices.""" registry = await entity_registry.async_get_registry(hass) triggers = [] @@ -39,28 +41,29 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: if entry.domain != DOMAIN: continue - triggers.append( + triggers += [ { CONF_PLATFORM: "device", CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "cleaning", + CONF_TYPE: trigger, } - ) - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "docked", - } - ) + for trigger in TRIGGER_TYPES + ] return triggers +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -68,8 +71,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Attach a trigger.""" - config = TRIGGER_SCHEMA(config) - if config[CONF_TYPE] == "cleaning": to_state = STATE_CLEANING else: @@ -80,6 +81,8 @@ async def async_attach_trigger( CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_trigger.CONF_TO: to_state, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] state_config = state_trigger.TRIGGER_SCHEMA(state_config) return await state_trigger.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index 48aa9615f1e..38958bd4790 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Vacuum state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -44,8 +46,8 @@ async def _async_reproduce_state( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -99,8 +101,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Vacuum states.""" # Reproduce states in parallel. diff --git a/homeassistant/components/vacuum/translations/id.json b/homeassistant/components/vacuum/translations/id.json index a9827363d5e..5fc888515fe 100644 --- a/homeassistant/components/vacuum/translations/id.json +++ b/homeassistant/components/vacuum/translations/id.json @@ -1,14 +1,28 @@ { + "device_automation": { + "action_type": { + "clean": "Perintahkan {entity_name} untuk bersih-bersih", + "dock": "Kembalikan {entity_name} ke dok" + }, + "condition_type": { + "is_cleaning": "{entity_name} sedang membersihkan", + "is_docked": "{entity_name} berlabuh di dok" + }, + "trigger_type": { + "cleaning": "{entity_name} mulai membersihkan", + "docked": "{entity_name} berlabuh" + } + }, "state": { "_": { "cleaning": "Membersihkan", "docked": "Berlabuh", "error": "Kesalahan", "idle": "Siaga", - "off": "Padam", + "off": "Mati", "on": "Nyala", - "paused": "Dijeda", - "returning": "Kembali ke dock" + "paused": "Jeda", + "returning": "Kembali ke dok" } }, "title": "Vakum" diff --git a/homeassistant/components/vacuum/translations/ko.json b/homeassistant/components/vacuum/translations/ko.json index 0e59d5157eb..52e1e1bd36b 100644 --- a/homeassistant/components/vacuum/translations/ko.json +++ b/homeassistant/components/vacuum/translations/ko.json @@ -1,16 +1,16 @@ { "device_automation": { "action_type": { - "clean": "{entity_name} \uc744(\ub97c) \uccad\uc18c\uc2dc\ud0a4\uae30", - "dock": "{entity_name} \uc744(\ub97c) \ucda9\uc804\uc2a4\ud14c\uc774\uc158\uc73c\ub85c \ubcf5\uadc0\uc2dc\ud0a4\uae30" + "clean": "{entity_name}\uc744(\ub97c) \uccad\uc18c\uc2dc\ud0a4\uae30", + "dock": "{entity_name}\uc744(\ub97c) \ucda9\uc804\uc2a4\ud14c\uc774\uc158\uc73c\ub85c \ubcf5\uadc0\uc2dc\ud0a4\uae30" }, "condition_type": { - "is_cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c \uc911\uc774\uba74", - "is_docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub418\uc5b4\uc788\uc73c\uba74" + "is_cleaning": "{entity_name}\uc774(\uac00) \uccad\uc18c \uc911\uc774\uba74", + "is_docked": "{entity_name}\uc774(\uac00) \ucda9\uc804 \uc2a4\ud14c\uc774\uc158\uc5d0 \uc788\uc73c\uba74" }, "trigger_type": { - "cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c\ub97c \uc2dc\uc791\ud560 \ub54c", - "docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub420 \ub54c" + "cleaning": "{entity_name}\uc774(\uac00) \uccad\uc18c\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c", + "docked": "{entity_name}\uc774(\uac00) \ucda9\uc804 \uc2a4\ud14c\uc774\uc158\uc5d0 \ubcf5\uadc0\ud588\uc744 \ub54c" } }, "state": { diff --git a/homeassistant/components/vacuum/translations/nl.json b/homeassistant/components/vacuum/translations/nl.json index 3fbb0ae50be..1de347f9875 100644 --- a/homeassistant/components/vacuum/translations/nl.json +++ b/homeassistant/components/vacuum/translations/nl.json @@ -25,5 +25,5 @@ "returning": "Terugkeren naar dock" } }, - "title": "Stofzuigen" + "title": "Stofzuiger" } \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/zh-Hans.json b/homeassistant/components/vacuum/translations/zh-Hans.json index 9e252236d0a..1e4be0ebe0b 100644 --- a/homeassistant/components/vacuum/translations/zh-Hans.json +++ b/homeassistant/components/vacuum/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "device_automation": { "action_type": { - "clean": "\u4f7f {entity_name} \u5f00\u59cb\u6e05\u626b" + "clean": "\u4f7f {entity_name} \u5f00\u59cb\u6e05\u626b", + "dock": "\u4f7f {entity_name} \u8fd4\u56de\u5e95\u5ea7" }, "condition_type": { "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u626b", diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 525bf00f50e..e167791e702 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -83,7 +83,7 @@ class ValloxFan(FanEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return { ATTR_PROFILE_FAN_SPEED_HOME["description"]: self._fan_speed_home, diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 6f6755a05e7..b4269ac4451 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -12,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from . import DOMAIN, METRIC_KEY_MODE, SIGNAL_VALLOX_STATE_UPDATE @@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensors, update_before_add=False) -class ValloxSensor(Entity): +class ValloxSensor(SensorEntity): """Representation of a Vallox sensor.""" def __init__( diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 882274f8e84..31c5da097ff 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -5,10 +5,9 @@ import logging import vasttrafik import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_DELAY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import now @@ -71,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class VasttrafikDepartureSensor(Entity): +class VasttrafikDepartureSensor(SensorEntity): """Implementation of a Vasttrafik Departure Sensor.""" def __init__(self, planner, name, departure, heading, lines, delay): @@ -106,7 +105,7 @@ class VasttrafikDepartureSensor(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index a859567e219..a15b0a641ef 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA ) -COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] +PLATFORMS = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] async def async_setup(hass, config): @@ -51,19 +51,19 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): def callback(): modules = controller.get_modules() discovery_info = {"cntrl": controller} - for category in COMPONENT_TYPES: - discovery_info[category] = [] + for platform in PLATFORMS: + discovery_info[platform] = [] for module in modules: for channel in range(1, module.number_of_channels() + 1): - for category in COMPONENT_TYPES: - if category in module.get_categories(channel): - discovery_info[category].append( + for platform in PLATFORMS: + if platform in module.get_categories(channel): + discovery_info[platform].append( (module.get_module_address(), channel) ) hass.data[DOMAIN][entry.entry_id] = discovery_info - for category in COMPONENT_TYPES: - hass.add_job(hass.config_entries.async_forward_entry_setup(entry, category)) + for platform in PLATFORMS: + hass.add_job(hass.config_entries.async_forward_entry_setup(entry, platform)) try: controller = velbus.Controller(entry.data[CONF_PORT]) @@ -113,8 +113,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Remove the velbus connection.""" await asyncio.wait( [ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in COMPONENT_TYPES + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) hass.data[DOMAIN][entry.entry_id]["cntrl"].stop() diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 095108b4401..9d9b68dd4eb 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,4 +1,5 @@ """Support for Velbus sensors.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR from . import VelbusEntity @@ -18,7 +19,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class VelbusSensor(VelbusEntity): +class VelbusSensor(VelbusEntity, SensorEntity): """Representation of a sensor.""" def __init__(self, module, channel, counter=False): diff --git a/homeassistant/components/velbus/translations/de.json b/homeassistant/components/velbus/translations/de.json index 9bbb23b1bcd..c6c452953ca 100644 --- a/homeassistant/components/velbus/translations/de.json +++ b/homeassistant/components/velbus/translations/de.json @@ -11,7 +11,7 @@ "user": { "data": { "name": "Der Name f\u00fcr diese Velbus-Verbindung", - "port": "Verbindungs details" + "port": "Verbindungsdetails" }, "title": "Definieren des Velbus-Verbindungstyps" } diff --git a/homeassistant/components/velbus/translations/hu.json b/homeassistant/components/velbus/translations/hu.json new file mode 100644 index 00000000000..414ee7e60c6 --- /dev/null +++ b/homeassistant/components/velbus/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/id.json b/homeassistant/components/velbus/translations/id.json new file mode 100644 index 00000000000..69a05411dc4 --- /dev/null +++ b/homeassistant/components/velbus/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "name": "Nama untuk koneksi velbus ini", + "port": "String koneksi" + }, + "title": "Tentukan jenis koneksi velbus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 90ed0a91b14..5c1d8bfd370 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -10,7 +10,7 @@ import homeassistant.helpers.config_validation as cv DOMAIN = "velux" DATA_VELUX = "data_velux" -SUPPORTED_DOMAINS = ["cover", "scene"] +PLATFORMS = ["cover", "scene"] _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( @@ -34,9 +34,9 @@ async def async_setup(hass, config): _LOGGER.exception("Can't connect to velux interface: %s", ex) return False - for component in SUPPORTED_DOMAINS: + for platform in PLATFORMS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, config) + discovery.async_load_platform(hass, platform, DOMAIN, {}, config) ) return True diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 261339f70dd..b4d8264a3ab 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -200,7 +200,7 @@ class VenstarThermostat(ClimateEntity): return FAN_AUTO @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" return { ATTR_FAN_STATE: self._client.fanstate, diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 4bfa72b5eb6..3654db5072d 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,8 +1,10 @@ """Support for Vera devices.""" +from __future__ import annotations + import asyncio from collections import defaultdict import logging -from typing import Any, Dict, Generic, List, Optional, Type, TypeVar +from typing import Any, Generic, TypeVar import pyvera as veraApi from requests.exceptions import RequestException @@ -172,7 +174,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -def map_vera_device(vera_device: veraApi.VeraDevice, remap: List[int]) -> str: +def map_vera_device(vera_device: veraApi.VeraDevice, remap: list[int]) -> str: """Map vera classes to Home Assistant types.""" type_map = { @@ -187,7 +189,7 @@ def map_vera_device(vera_device: veraApi.VeraDevice, remap: List[int]) -> str: veraApi.VeraSwitch: "switch", } - def map_special_case(instance_class: Type, entity_type: str) -> str: + def map_special_case(instance_class: type, entity_type: str) -> str: if instance_class is veraApi.VeraSwitch and vera_device.device_id in remap: return "light" return entity_type @@ -234,7 +236,9 @@ class VeraDevice(Generic[DeviceType], Entity): def update(self): """Force a refresh from the device if the device is unavailable.""" - if not self.available: + refresh_needed = self.vera_device.should_poll or not self.available + _LOGGER.debug("%s: update called (refresh=%s)", self._name, refresh_needed) + if refresh_needed: self.vera_device.refresh() @property @@ -243,12 +247,7 @@ class VeraDevice(Generic[DeviceType], Entity): return self._name @property - def should_poll(self) -> bool: - """Get polling requirement from vera device.""" - return self.vera_device.should_poll - - @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the device.""" attr = {} diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 00d4fb3a758..816234bb602 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,5 +1,7 @@ """Support for Vera binary sensors.""" -from typing import Callable, List, Optional +from __future__ import annotations + +from typing import Callable import pyvera as veraApi @@ -19,7 +21,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -44,7 +46,7 @@ class VeraBinarySensor(VeraDevice[veraApi.VeraBinarySensor], BinarySensorEntity) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._state diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 9abfc485268..5027becb71f 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,5 +1,7 @@ """Support for Vera thermostats.""" -from typing import Any, Callable, List, Optional +from __future__ import annotations + +from typing import Any, Callable import pyvera as veraApi @@ -36,7 +38,7 @@ SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_O async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -60,7 +62,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int | None: """Return the list of supported features.""" return SUPPORT_FLAGS @@ -80,7 +82,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): return HVAC_MODE_OFF @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. @@ -88,7 +90,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): return SUPPORT_HVAC @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting.""" mode = self.vera_device.get_fan_mode() if mode == "ContinuousOn": @@ -96,7 +98,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): return FAN_AUTO @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return a list of available fan modes.""" return FAN_OPERATION_LIST @@ -110,7 +112,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): self.schedule_update_ha_state() @property - def current_power_w(self) -> Optional[float]: + def current_power_w(self) -> float | None: """Return the current power usage in W.""" power = self.vera_device.power if power: @@ -127,7 +129,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): return TEMP_CELSIUS @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self.vera_device.get_current_temperature() @@ -137,7 +139,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): return self.vera_device.get_hvac_mode() @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self.vera_device.get_current_goal_temperature() diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index fce6475f930..fcc501c2094 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -1,5 +1,7 @@ """Common vera code.""" -from typing import DefaultDict, List, NamedTuple, Set +from __future__ import annotations + +from typing import DefaultDict, NamedTuple import pyvera as pv @@ -15,12 +17,12 @@ class ControllerData(NamedTuple): """Controller data.""" controller: pv.VeraController - devices: DefaultDict[str, List[pv.VeraDevice]] - scenes: List[pv.VeraScene] + devices: DefaultDict[str, list[pv.VeraDevice]] + scenes: list[pv.VeraScene] config_entry: ConfigEntry -def get_configured_platforms(controller_data: ControllerData) -> Set[str]: +def get_configured_platforms(controller_data: ControllerData) -> set[str]: """Get configured platforms for a controller.""" platforms = [] for platform in controller_data.devices: diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 754d2eca542..a5450cd4a65 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,7 +1,9 @@ """Config flow for Vera.""" +from __future__ import annotations + import logging import re -from typing import Any, List +from typing import Any import pyvera as pv from requests.exceptions import RequestException @@ -13,32 +15,28 @@ from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback from homeassistant.helpers.entity_registry import EntityRegistry -from .const import ( # pylint: disable=unused-import - CONF_CONTROLLER, - CONF_LEGACY_UNIQUE_ID, - DOMAIN, -) +from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN LIST_REGEX = re.compile("[^0-9]+") _LOGGER = logging.getLogger(__name__) -def fix_device_id_list(data: List[Any]) -> List[int]: +def fix_device_id_list(data: list[Any]) -> list[int]: """Fix the id list by converting it to a supported int list.""" return str_to_int_list(list_to_str(data)) -def str_to_int_list(data: str) -> List[int]: +def str_to_int_list(data: str) -> list[int]: """Convert a string to an int list.""" return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] -def list_to_str(data: List[Any]) -> str: +def list_to_str(data: list[Any]) -> str: """Convert an int list to a string.""" return " ".join([str(i) for i in data]) -def new_options(lights: List[int], exclude: List[int]) -> dict: +def new_options(lights: list[int], exclude: list[int]) -> dict: """Create a standard options object.""" return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 43f68fba786..cf3dd4a3d13 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,5 +1,7 @@ """Support for Vera cover - curtains, rollershutters etc.""" -from typing import Any, Callable, List +from __future__ import annotations + +from typing import Any, Callable import pyvera as veraApi @@ -20,7 +22,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 30c4e93a2ba..7fcb726efcc 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,5 +1,7 @@ """Support for Vera lights.""" -from typing import Any, Callable, List, Optional, Tuple +from __future__ import annotations + +from typing import Any, Callable import pyvera as veraApi @@ -24,7 +26,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -51,12 +53,12 @@ class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of the light.""" return self._brightness @property - def hs_color(self) -> Optional[Tuple[float, float]]: + def hs_color(self) -> tuple[float, float] | None: """Return the color of the light.""" return self._color diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index b77f17d3b0a..eada4b20550 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,5 +1,7 @@ """Support for Vera locks.""" -from typing import Any, Callable, Dict, List, Optional +from __future__ import annotations + +from typing import Any, Callable import pyvera as veraApi @@ -23,7 +25,7 @@ ATTR_LOW_BATTERY = "low_battery" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -56,19 +58,19 @@ class VeraLock(VeraDevice[veraApi.VeraLock], LockEntity): self._state = STATE_UNLOCKED @property - def is_locked(self) -> Optional[bool]: + def is_locked(self) -> bool | None: """Return true if device is on.""" return self._state == STATE_LOCKED @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Who unlocked the lock and did a low battery alert fire. Reports on the previous poll cycle. changed_by_name is a string like 'Bob'. low_battery is 1 if an alert fired, 0 otherwise. """ - data = super().device_state_attributes + data = super().extra_state_attributes last_user = self.vera_device.get_last_user_alert() if last_user is not None: @@ -78,7 +80,7 @@ class VeraLock(VeraDevice[veraApi.VeraLock], LockEntity): return data @property - def changed_by(self) -> Optional[str]: + def changed_by(self) -> str | None: """Who unlocked the lock. Reports on the previous poll cycle. diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 1f180b39750..76d6bda5c7b 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.13"], - "codeowners": ["@vangorra"] + "codeowners": ["@pavoni"] } diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index 2274b67f683..c6eb983a8f7 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,5 +1,7 @@ """Support for Vera scenes.""" -from typing import Any, Callable, Dict, List, Optional +from __future__ import annotations + +from typing import Any, Callable import pyvera as veraApi @@ -16,7 +18,7 @@ from .const import VERA_ID_FORMAT async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -53,6 +55,6 @@ class VeraScene(Scene): return self._name @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the scene.""" return {"vera_scene_id": self.vera_scene.vera_scene_id} diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 007290807e6..516801b57c6 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -1,10 +1,16 @@ """Support for Vera sensors.""" +from __future__ import annotations + from datetime import timedelta -from typing import Callable, List, Optional, cast +from typing import Callable, cast import pyvera as veraApi -from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT +from homeassistant.components.sensor import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant @@ -20,7 +26,7 @@ SCAN_INTERVAL = timedelta(seconds=5) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -33,7 +39,7 @@ async def async_setup_entry( ) -class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity): +class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity): """Representation of a Vera Sensor.""" def __init__( @@ -52,7 +58,7 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], Entity): return self.current_value @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index f567893e5b0..c779a3c8cfc 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,5 +1,7 @@ """Support for Vera switches.""" -from typing import Any, Callable, List, Optional +from __future__ import annotations + +from typing import Any, Callable import pyvera as veraApi @@ -20,7 +22,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -57,7 +59,7 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): self.schedule_update_ha_state() @property - def current_power_w(self) -> Optional[float]: + def current_power_w(self) -> float | None: """Return the current power usage in W.""" power = self.vera_device.power if power: diff --git a/homeassistant/components/vera/translations/id.json b/homeassistant/components/vera/translations/id.json new file mode 100644 index 00000000000..435fc722dba --- /dev/null +++ b/homeassistant/components/vera/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "Tidak dapat terhubung ke pengontrol dengan URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "ID perangkat Vera yang akan dikecualikan dari Home Assistant.", + "lights": "ID perangkat sakelar Vera yang diperlakukan sebagai lampu di Home Assistant", + "vera_controller_url": "URL Pengontrol" + }, + "description": "Tentukan URL pengontrol Vera di bawah. Ini akan terlihat seperti ini: http://192.168.1.161:3480.", + "title": "Siapkan pengontrol Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "ID perangkat Vera yang akan dikecualikan dari Home Assistant.", + "lights": "ID perangkat sakelar Vera yang diperlakukan sebagai lampu di Home Assistant" + }, + "description": "Lihat dokumentasi Vera untuk informasi tentang parameter opsional: https://www.home-assistant.io/integrations/vera/. Catatan: Setiap perubahan yang dilakukan di sini membutuhkan memulai ulang Home Assistant. Untuk menghapus nilai, ketikkan karakter spasi.", + "title": "Opsi pengontrol Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/ko.json b/homeassistant/components/vera/translations/ko.json index e8658528488..0990435cb4a 100644 --- a/homeassistant/components/vera/translations/ko.json +++ b/homeassistant/components/vera/translations/ko.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "exclude": "Home Assistant \uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", - "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.", + "exclude": "Home Assistant\uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", + "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant\uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.", "vera_controller_url": "\ucee8\ud2b8\ub864\ub7ec URL" }, "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", @@ -19,8 +19,8 @@ "step": { "init": { "data": { - "exclude": "Home Assistant \uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", - "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4." + "exclude": "Home Assistant\uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", + "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant\uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4." }, "description": "\ub9e4\uac1c \ubcc0\uc218 \uc120\ud0dd\uc0ac\ud56d\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 vera \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/vera/. \ucc38\uace0: \uc5ec\uae30\uc5d0\uc11c \ubcc0\uacbd\ud558\uba74 Home Assistant \uc11c\ubc84\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \uac12\uc744 \uc9c0\uc6b0\ub824\uba74 \uc785\ub825\ub780\uc744 \uacf5\ubc31\uc73c\ub85c \ub450\uc138\uc694.", "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc635\uc158" diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 2348d42a0d3..32893aec88b 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,223 +1,173 @@ """Support for Verisure devices.""" -from datetime import timedelta +from __future__ import annotations + +import asyncio +from contextlib import suppress +import os +from typing import Any -from jsonpath import jsonpath -import verisure import voluptuous as vol +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.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_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_IMPORT, SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( + CONF_EMAIL, CONF_PASSWORD, - CONF_SCAN_INTERVAL, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - HTTP_SERVICE_UNAVAILABLE, ) -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( - ATTR_DEVICE_SERIAL, - CONF_ALARM, CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, - CONF_DOOR_WINDOW, CONF_GIID, - CONF_HYDROMETERS, - CONF_LOCKS, - CONF_MOUSE, - CONF_SMARTCAM, - CONF_SMARTPLUGS, - CONF_THERMOMETERS, - DEFAULT_SCAN_INTERVAL, + CONF_LOCK_CODE_DIGITS, + CONF_LOCK_DEFAULT_CODE, + DEFAULT_LOCK_CODE_DIGITS, DOMAIN, - LOGGER, - MIN_SCAN_INTERVAL, - SERVICE_CAPTURE_SMARTCAM, - SERVICE_DISABLE_AUTOLOCK, - SERVICE_ENABLE_AUTOLOCK, ) +from .coordinator import VerisureDataUpdateCoordinator -HUB = None +PLATFORMS = [ + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + CAMERA_DOMAIN, + LOCK_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_ALARM, default=True): cv.boolean, - vol.Optional(CONF_CODE_DIGITS, default=4): cv.positive_int, - vol.Optional(CONF_DOOR_WINDOW, default=True): cv.boolean, - vol.Optional(CONF_GIID): cv.string, - vol.Optional(CONF_HYDROMETERS, default=True): cv.boolean, - vol.Optional(CONF_LOCKS, default=True): cv.boolean, - vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string, - vol.Optional(CONF_MOUSE, default=True): cv.boolean, - vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, - vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, - vol.Optional(CONF_SMARTCAM, default=True): cv.boolean, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): ( - vol.All(cv.time_period, vol.Clamp(min=MIN_SCAN_INTERVAL)) - ), - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_CODE_DIGITS): cv.positive_int, + vol.Optional(CONF_GIID): cv.string, + vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string, + }, + extra=vol.ALLOW_EXTRA, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -DEVICE_SERIAL_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) +async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: + """Set up the Verisure integration.""" + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_EMAIL: config[DOMAIN][CONF_USERNAME], + CONF_PASSWORD: config[DOMAIN][CONF_PASSWORD], + CONF_GIID: config[DOMAIN].get(CONF_GIID), + CONF_LOCK_CODE_DIGITS: config[DOMAIN].get(CONF_CODE_DIGITS), + CONF_LOCK_DEFAULT_CODE: config[DOMAIN].get(CONF_LOCK_DEFAULT_CODE), + }, + ) + ) -def setup(hass, config): - """Set up the Verisure component.""" - global HUB # pylint: disable=global-statement - HUB = VerisureHub(config[DOMAIN]) - HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])( - HUB.update_overview - ) - if not HUB.login(): - return False - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: HUB.logout()) - HUB.update_overview() - - for component in ( - "sensor", - "switch", - "alarm_control_panel", - "lock", - "camera", - "binary_sensor", - ): - discovery.load_platform(hass, component, DOMAIN, {}, config) - - async def capture_smartcam(service): - """Capture a new picture from a smartcam.""" - device_id = service.data[ATTR_DEVICE_SERIAL] - try: - await hass.async_add_executor_job(HUB.smartcam_capture, device_id) - LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) - except verisure.Error as ex: - LOGGER.error("Could not capture image, %s", ex) - - hass.services.register( - DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA - ) - - async def disable_autolock(service): - """Disable autolock on a doorlock.""" - device_id = service.data[ATTR_DEVICE_SERIAL] - try: - await hass.async_add_executor_job(HUB.disable_autolock, device_id) - LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) - except verisure.Error as ex: - LOGGER.error("Could not disable autolock, %s", ex) - - hass.services.register( - DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA - ) - - async def enable_autolock(service): - """Enable autolock on a doorlock.""" - device_id = service.data[ATTR_DEVICE_SERIAL] - try: - await hass.async_add_executor_job(HUB.enable_autolock, device_id) - LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) - except verisure.Error as ex: - LOGGER.error("Could not enable autolock, %s", ex) - - hass.services.register( - DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA - ) return True -class VerisureHub: - """A Verisure hub wrapper class.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Verisure from a config entry.""" + # Migrate old YAML settings (hidden in the config entry), + # to config entry options. Can be removed after YAML support is gone. + if CONF_LOCK_CODE_DIGITS in entry.data or CONF_DEFAULT_LOCK_CODE in entry.data: + options = entry.options.copy() - def __init__(self, domain_config): - """Initialize the Verisure hub.""" - self.overview = {} - self.imageseries = {} + if ( + CONF_LOCK_CODE_DIGITS in entry.data + and CONF_LOCK_CODE_DIGITS not in entry.options + and entry.data[CONF_LOCK_CODE_DIGITS] != DEFAULT_LOCK_CODE_DIGITS + ): + options.update( + { + CONF_LOCK_CODE_DIGITS: entry.data[CONF_LOCK_CODE_DIGITS], + } + ) - self.config = domain_config + if ( + CONF_DEFAULT_LOCK_CODE in entry.data + and CONF_DEFAULT_LOCK_CODE not in entry.options + ): + options.update( + { + CONF_DEFAULT_LOCK_CODE: entry.data[CONF_DEFAULT_LOCK_CODE], + } + ) - self.session = verisure.Session( - domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD] + data = entry.data.copy() + data.pop(CONF_LOCK_CODE_DIGITS, None) + data.pop(CONF_DEFAULT_LOCK_CODE, None) + hass.config_entries.async_update_entry(entry, data=data, options=options) + + # Continue as normal... + coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) + + if not await coordinator.async_login(): + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={"entry": entry}, + ) + return False + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + # Set up all platforms for this device/entry. + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) ) - self.giid = domain_config.get(CONF_GIID) + return True - def login(self): - """Login to Verisure.""" - try: - self.session.login() - except verisure.Error as ex: - LOGGER.error("Could not log in to verisure, %s", ex) - return False - if self.giid: - return self.set_giid() - return True - def logout(self): - """Logout from Verisure.""" - try: - self.session.logout() - except verisure.Error as ex: - LOGGER.error("Could not log out from verisure, %s", ex) - return False - return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Verisure config entry.""" + unload_ok = all( + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ) + ) + ) - def set_giid(self): - """Set installation GIID.""" - try: - self.session.set_giid(self.giid) - except verisure.Error as ex: - LOGGER.error("Could not set installation GIID, %s", ex) - return False - return True + if not unload_ok: + return False - def update_overview(self): - """Update the overview.""" - try: - self.overview = self.session.get_overview() - except verisure.ResponseError as ex: - LOGGER.error("Could not read overview, %s", ex) - if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable - LOGGER.info("Trying to log in again") - self.login() - else: - raise + cookie_file = hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}") + with suppress(FileNotFoundError): + await hass.async_add_executor_job(os.unlink, cookie_file) - @Throttle(timedelta(seconds=60)) - def update_smartcam_imageseries(self): - """Update the image series.""" - self.imageseries = self.session.get_camera_imageseries() + del hass.data[DOMAIN][entry.entry_id] - @Throttle(timedelta(seconds=30)) - def smartcam_capture(self, device_id): - """Capture a new image from a smartcam.""" - self.session.capture_image(device_id) + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] - def disable_autolock(self, device_id): - """Disable autolock.""" - self.session.set_lock_config(device_id, auto_lock_enabled=False) - - def enable_autolock(self, device_id): - """Enable autolock.""" - self.session.set_lock_config(device_id, auto_lock_enabled=True) - - def get(self, jpath, *args): - """Get values from the overview that matches the jsonpath.""" - res = jsonpath(self.overview, jpath % args) - return res or [] - - def get_first(self, jpath, *args): - """Get first value from the overview that matches the jsonpath.""" - res = self.get(jpath, *args) - return res[0] if res else None - - def get_image_info(self, jpath, *args): - """Get values from the imageseries that matches the jsonpath.""" - res = jsonpath(self.imageseries, jpath % args) - return res or [] + return True diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index fff58433a9c..34a60b9cae4 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -1,68 +1,66 @@ """Support for Verisure alarm control panels.""" -from time import sleep +from __future__ import annotations -import homeassistant.components.alarm_control_panel as alarm +import asyncio +from typing import Any, Callable, Iterable + +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanelEntity, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import HUB as hub -from .const import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, LOGGER +from .const import ALARM_STATE_TO_HA, CONF_GIID, DOMAIN, LOGGER +from .coordinator import VerisureDataUpdateCoordinator -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure platform.""" - alarms = [] - if int(hub.config.get(CONF_ALARM, 1)): - hub.update_overview() - alarms.append(VerisureAlarm()) - add_entities(alarms) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: + """Set up Verisure alarm control panel from a config entry.""" + async_add_entities([VerisureAlarm(coordinator=hass.data[DOMAIN][entry.entry_id])]) -def set_arm_state(state, code=None): - """Send set arm state command.""" - transaction_id = hub.session.set_arm_state(code, state)[ - "armStateChangeTransactionId" - ] - LOGGER.info("verisure set arm state %s", state) - transaction = {} - while "result" not in transaction: - sleep(0.5) - transaction = hub.session.get_arm_state_transaction(transaction_id) - hub.update_overview() - - -class VerisureAlarm(alarm.AlarmControlPanelEntity): +class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): """Representation of a Verisure alarm status.""" - def __init__(self): - """Initialize the Verisure alarm panel.""" - self._state = None - self._digits = hub.config.get(CONF_CODE_DIGITS) - self._changed_by = None + coordinator: VerisureDataUpdateCoordinator + + _changed_by: str | None = None + _state: str | None = None @property - def name(self): - """Return the name of the device.""" - giid = hub.config.get(CONF_GIID) - if giid is not None: - aliass = {i["giid"]: i["alias"] for i in hub.session.installations} - if giid in aliass: - return "{} alarm".format(aliass[giid]) - - LOGGER.error("Verisure installation giid not found: %s", giid) - - return "{} alarm".format(hub.session.installations[0]["alias"]) + def name(self) -> str: + """Return the name of the entity.""" + return "Verisure Alarm" @property - def state(self): - """Return the state of the device.""" + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self.coordinator.entry.data[CONF_GIID] + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + return { + "name": "Verisure Alarm", + "manufacturer": "Verisure", + "model": "VBox", + "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, + } + + @property + def state(self) -> str | None: + """Return the state of the entity.""" return self._state @property @@ -71,37 +69,53 @@ class VerisureAlarm(alarm.AlarmControlPanelEntity): return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY @property - def code_format(self): + def code_format(self) -> str: """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property - def changed_by(self): + def changed_by(self) -> str | None: """Return the last change triggered by.""" return self._changed_by - def update(self): - """Update alarm status.""" - hub.update_overview() - status = hub.get_first("$.armState.statusType") - if status == "DISARMED": - self._state = STATE_ALARM_DISARMED - elif status == "ARMED_HOME": - self._state = STATE_ALARM_ARMED_HOME - elif status == "ARMED_AWAY": - self._state = STATE_ALARM_ARMED_AWAY - elif status != "PENDING": - LOGGER.error("Unknown alarm state %s", status) - self._changed_by = hub.get_first("$.armState.name") + async def _async_set_arm_state(self, state: str, code: str | None = None) -> None: + """Send set arm state command.""" + arm_state = await self.hass.async_add_executor_job( + self.coordinator.verisure.set_arm_state, code, state + ) + LOGGER.debug("Verisure set arm state %s", state) + transaction = {} + while "result" not in transaction: + await asyncio.sleep(0.5) + transaction = await self.hass.async_add_executor_job( + self.coordinator.verisure.get_arm_state_transaction, + arm_state["armStateChangeTransactionId"], + ) - def alarm_disarm(self, code=None): + await self.coordinator.async_refresh() + + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - set_arm_state("DISARMED", code) + await self._async_set_arm_state("DISARMED", code) - def alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - set_arm_state("ARMED_HOME", code) + await self._async_set_arm_state("ARMED_HOME", code) - def alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - set_arm_state("ARMED_AWAY", code) + await self._async_set_arm_state("ARMED_AWAY", code) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._state = ALARM_STATE_TO_HA.get( + self.coordinator.data["alarm"]["statusType"] + ) + self._changed_by = self.coordinator.data["alarm"]["name"] + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 5a7f4386ece..3363178efe2 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,98 +1,132 @@ """Support for Verisure binary sensors.""" +from __future__ import annotations + +from typing import Any, Callable, Iterable + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_OPENING, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CONF_DOOR_WINDOW, HUB as hub +from .const import CONF_GIID, DOMAIN +from .coordinator import VerisureDataUpdateCoordinator -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure binary sensors.""" - sensors = [] - hub.update_overview() +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: + """Set up Verisure binary sensors based on a config entry.""" + coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - if int(hub.config.get(CONF_DOOR_WINDOW, 1)): - sensors.extend( - [ - VerisureDoorWindowSensor(device_label) - for device_label in hub.get( - "$.doorWindow.doorWindowDevice[*].deviceLabel" - ) - ] - ) + sensors: list[Entity] = [VerisureEthernetStatus(coordinator)] - sensors.extend([VerisureEthernetStatus()]) - add_entities(sensors) + sensors.extend( + VerisureDoorWindowSensor(coordinator, serial_number) + for serial_number in coordinator.data["door_window"] + ) + + async_add_entities(sensors) -class VerisureDoorWindowSensor(BinarySensorEntity): +class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): """Representation of a Verisure door window sensor.""" - def __init__(self, device_label): + coordinator: VerisureDataUpdateCoordinator + + def __init__( + self, coordinator: VerisureDataUpdateCoordinator, serial_number: str + ) -> None: """Initialize the Verisure door window sensor.""" - self._device_label = device_label + super().__init__(coordinator) + self.serial_number = serial_number @property - def name(self): - """Return the name of the binary sensor.""" - return hub.get_first( - "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area", - self._device_label, - ) + def name(self) -> str: + """Return the name of this entity.""" + return self.coordinator.data["door_window"][self.serial_number]["area"] @property - def is_on(self): + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return f"{self.serial_number}_door_window" + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + area = self.coordinator.data["door_window"][self.serial_number]["area"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": "Shock Sensor Detector", + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } + + @property + def device_class(self) -> str: + """Return the class of this entity.""" + return DEVICE_CLASS_OPENING + + @property + def is_on(self) -> bool: """Return the state of the sensor.""" return ( - hub.get_first( - "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state", - self._device_label, - ) - == "OPEN" + self.coordinator.data["door_window"][self.serial_number]["state"] == "OPEN" ) @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return ( - hub.get_first( - "$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]", - self._device_label, - ) - is not None + super().available + and self.serial_number in self.coordinator.data["door_window"] ) - # pylint: disable=no-self-use - def update(self): - """Update the state of the sensor.""" - hub.update_overview() - -class VerisureEthernetStatus(BinarySensorEntity): +class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): """Representation of a Verisure VBOX internet status.""" + coordinator: VerisureDataUpdateCoordinator + @property - def name(self): - """Return the name of the binary sensor.""" + def name(self) -> str: + """Return the name of this entity.""" return "Verisure Ethernet status" @property - def is_on(self): + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return f"{self.coordinator.entry.data[CONF_GIID]}_ethernet" + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + return { + "name": "Verisure Alarm", + "manufacturer": "Verisure", + "model": "VBox", + "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, + } + + @property + def is_on(self) -> bool: """Return the state of the sensor.""" - return hub.get_first("$.ethernetConnectedNow") + return self.coordinator.data["ethernet"] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" - return hub.get_first("$.ethernetConnectedNow") is not None - - # pylint: disable=no-self-use - def update(self): - """Update the state of the sensor.""" - hub.update_overview() + return super().available and self.coordinator.data["ethernet"] is not None @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" + def device_class(self) -> str: + """Return the class of this entity.""" return DEVICE_CLASS_CONNECTIVITY diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a69e1fb95d8..cb159027c16 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,45 +1,89 @@ """Support for Verisure cameras.""" +from __future__ import annotations + import errno import os +from typing import Any, Callable, Iterable + +from verisure import Error as VerisureError from homeassistant.components.camera import Camera +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.entity_platform import current_platform +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import HUB as hub -from .const import CONF_SMARTCAM, LOGGER +from .const import CONF_GIID, DOMAIN, LOGGER, SERVICE_CAPTURE_SMARTCAM +from .coordinator import VerisureDataUpdateCoordinator -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure Camera.""" - if not int(hub.config.get(CONF_SMARTCAM, 1)): - return False - directory_path = hass.config.config_dir - if not os.access(directory_path, os.R_OK): - LOGGER.error("file path %s is not readable", directory_path) - return False - hub.update_overview() - smartcams = [ - VerisureSmartcam(hass, device_label, directory_path) - for device_label in hub.get("$.customerImageCameras[*].deviceLabel") - ] +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: + """Set up Verisure sensors based on a config entry.""" + coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - add_entities(smartcams) + platform = current_platform.get() + platform.async_register_entity_service( + SERVICE_CAPTURE_SMARTCAM, + {}, + VerisureSmartcam.capture_smartcam.__name__, + ) + + assert hass.config.config_dir + async_add_entities( + VerisureSmartcam(coordinator, serial_number, hass.config.config_dir) + for serial_number in coordinator.data["cameras"] + ) -class VerisureSmartcam(Camera): +class VerisureSmartcam(CoordinatorEntity, Camera): """Representation of a Verisure camera.""" - def __init__(self, hass, device_label, directory_path): - """Initialize Verisure File Camera component.""" - super().__init__() + coordinator = VerisureDataUpdateCoordinator - self._device_label = device_label + def __init__( + self, + coordinator: VerisureDataUpdateCoordinator, + serial_number: str, + directory_path: str, + ): + """Initialize Verisure File Camera component.""" + super().__init__(coordinator) + + self.serial_number = serial_number self._directory_path = directory_path self._image = None self._image_id = None - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image) - def camera_image(self): + @property + def name(self) -> str: + """Return the name of this entity.""" + return self.coordinator.data["cameras"][self.serial_number]["area"] + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self.serial_number + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + area = self.coordinator.data["cameras"][self.serial_number]["area"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": "SmartCam", + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } + + def camera_image(self) -> bytes | None: """Return image response.""" self.check_imagelist() if not self._image: @@ -49,30 +93,38 @@ class VerisureSmartcam(Camera): with open(self._image, "rb") as file: return file.read() - def check_imagelist(self): + def check_imagelist(self) -> None: """Check the contents of the image list.""" - hub.update_smartcam_imageseries() - image_ids = hub.get_image_info( - "$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId", self._device_label - ) - if not image_ids: + self.coordinator.update_smartcam_imageseries() + + images = self.coordinator.imageseries.get("imageSeries", []) + new_image_id = None + for image in images: + if image["deviceLabel"] == self.serial_number: + new_image_id = image["image"][0]["imageId"] + break + + if not new_image_id: return - new_image_id = image_ids[0] + if new_image_id in ("-1", self._image_id): LOGGER.debug("The image is the same, or loading image_id") return + LOGGER.debug("Download new image %s", new_image_id) new_image_path = os.path.join( self._directory_path, "{}{}".format(new_image_id, ".jpg") ) - hub.session.download_image(self._device_label, new_image_id, new_image_path) + self.coordinator.verisure.download_image( + self.serial_number, new_image_id, new_image_path + ) LOGGER.debug("Old image_id=%s", self._image_id) - self.delete_image(self) + self.delete_image() self._image_id = new_image_id self._image = new_image_path - def delete_image(self, event): + def delete_image(self, _=None) -> None: """Delete an old image.""" remove_image = os.path.join( self._directory_path, "{}{}".format(self._image_id, ".jpg") @@ -84,9 +136,15 @@ class VerisureSmartcam(Camera): if error.errno != errno.ENOENT: raise - @property - def name(self): - """Return the name of this camera.""" - return hub.get_first( - "$.customerImageCameras[?(@.deviceLabel=='%s')].area", self._device_label - ) + def capture_smartcam(self) -> None: + """Capture a new picture from a smartcam.""" + try: + self.coordinator.smartcam_capture(self.serial_number) + LOGGER.debug("Capturing new image from %s", self.serial_number) + except VerisureError as ex: + LOGGER.error("Could not capture image, %s", ex) + + async def async_added_to_hass(self) -> None: + """Entity added to Home Assistant.""" + await super().async_added_to_hass() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.delete_image) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py new file mode 100644 index 00000000000..25560b62b16 --- /dev/null +++ b/homeassistant/components/verisure/config_flow.py @@ -0,0 +1,236 @@ +"""Config flow for Verisure integration.""" +from __future__ import annotations + +from typing import Any + +from verisure import ( + Error as VerisureError, + LoginError as VerisureLoginError, + ResponseError as VerisureResponseError, + Session as Verisure, +) +import voluptuous as vol + +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_POLL, + ConfigEntry, + ConfigFlow, + OptionsFlow, +) +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import callback + +from .const import ( + CONF_GIID, + CONF_LOCK_CODE_DIGITS, + CONF_LOCK_DEFAULT_CODE, + DEFAULT_LOCK_CODE_DIGITS, + DOMAIN, + LOGGER, +) + + +class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Verisure.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL + + email: str + entry: ConfigEntry + installations: dict[str, str] + password: str + + # These can be removed after YAML import has been removed. + giid: str | None = None + settings: dict[str, int | str] + + def __init__(self): + """Initialize.""" + self.settings = {} + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> VerisureOptionsFlowHandler: + """Get the options flow for this handler.""" + return VerisureOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Handle the initial step.""" + errors: dict[str, str] = {} + + if user_input is not None: + verisure = Verisure( + username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + ) + try: + await self.hass.async_add_executor_job(verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + self.installations = { + inst["giid"]: f"{inst['alias']} ({inst['street']})" + for inst in verisure.installations + } + + return await self.async_step_installation() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def async_step_installation( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Select Verisure installation to add.""" + if len(self.installations) == 1: + user_input = {CONF_GIID: list(self.installations)[0]} + elif self.giid and self.giid in self.installations: + user_input = {CONF_GIID: self.giid} + + if user_input is None: + return self.async_show_form( + step_id="installation", + data_schema=vol.Schema( + {vol.Required(CONF_GIID): vol.In(self.installations)} + ), + ) + + await self.async_set_unique_id(user_input[CONF_GIID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.installations[user_input[CONF_GIID]], + data={ + CONF_EMAIL: self.email, + CONF_PASSWORD: self.password, + CONF_GIID: user_input[CONF_GIID], + **self.settings, + }, + ) + + async def async_step_reauth(self, data: dict[str, Any]) -> dict[str, Any]: + """Handle initiation of re-authentication with Verisure.""" + self.entry = data["entry"] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Handle re-authentication with Verisure.""" + errors: dict[str, str] = {} + + if user_input is not None: + verisure = Verisure( + username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + ) + try: + await self.hass.async_add_executor_job(verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + data = self.entry.data.copy() + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **data, + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL, default=self.entry.data[CONF_EMAIL]): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> dict[str, Any]: + """Import Verisure YAML configuration.""" + if user_input[CONF_GIID]: + self.giid = user_input[CONF_GIID] + await self.async_set_unique_id(self.giid) + self._abort_if_unique_id_configured() + else: + # The old YAML configuration could handle 1 single Verisure instance. + # Therefore, if we don't know the GIID, we can use the discovery + # without a unique ID logic, to prevent re-import/discovery. + await self._async_handle_discovery_without_unique_id() + + # Settings, later to be converted to config entry options + if user_input[CONF_LOCK_CODE_DIGITS]: + self.settings[CONF_LOCK_CODE_DIGITS] = user_input[CONF_LOCK_CODE_DIGITS] + if user_input[CONF_LOCK_DEFAULT_CODE]: + self.settings[CONF_LOCK_DEFAULT_CODE] = user_input[CONF_LOCK_DEFAULT_CODE] + + return await self.async_step_user(user_input) + + +class VerisureOptionsFlowHandler(OptionsFlow): + """Handle Verisure options.""" + + def __init__(self, entry: ConfigEntry) -> None: + """Initialize Verisure options flow.""" + self.entry = entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Manage Verisure options.""" + errors = {} + + if user_input is not None: + if len(user_input[CONF_LOCK_DEFAULT_CODE]) not in [ + 0, + user_input[CONF_LOCK_CODE_DIGITS], + ]: + errors["base"] = "code_format_mismatch" + else: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_LOCK_CODE_DIGITS, + default=self.entry.options.get( + CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS + ), + ): int, + vol.Optional( + CONF_LOCK_DEFAULT_CODE, + default=self.entry.options.get(CONF_LOCK_DEFAULT_CODE), + ): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py index 89dcfa396aa..030c5a58075 100644 --- a/homeassistant/components/verisure/const.py +++ b/homeassistant/components/verisure/const.py @@ -2,27 +2,49 @@ from datetime import timedelta import logging +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, +) + DOMAIN = "verisure" LOGGER = logging.getLogger(__package__) -ATTR_DEVICE_SERIAL = "device_serial" - -CONF_ALARM = "alarm" -CONF_CODE_DIGITS = "code_digits" -CONF_DOOR_WINDOW = "door_window" CONF_GIID = "giid" -CONF_HYDROMETERS = "hygrometers" -CONF_LOCKS = "locks" -CONF_DEFAULT_LOCK_CODE = "default_lock_code" -CONF_MOUSE = "mouse" -CONF_SMARTPLUGS = "smartplugs" -CONF_THERMOMETERS = "thermometers" -CONF_SMARTCAM = "smartcam" +CONF_LOCK_CODE_DIGITS = "lock_code_digits" +CONF_LOCK_DEFAULT_CODE = "lock_default_code" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) -MIN_SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_LOCK_CODE_DIGITS = 4 SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" SERVICE_DISABLE_AUTOLOCK = "disable_autolock" SERVICE_ENABLE_AUTOLOCK = "enable_autolock" + +# Mapping of device types to a human readable name +DEVICE_TYPE_NAME = { + "CAMERAPIR2": "Camera detector", + "HOMEPAD1": "VoiceBox", + "HUMIDITY1": "Climate sensor", + "PIR2": "Camera detector", + "SIREN1": "Siren", + "SMARTCAMERA1": "SmartCam", + "SMOKE2": "Smoke detector", + "SMOKE3": "Smoke detector", + "VOICEBOX1": "VoiceBox", + "WATER1": "Water detector", +} + +ALARM_STATE_TO_HA = { + "DISARMED": STATE_ALARM_DISARMED, + "ARMED_HOME": STATE_ALARM_ARMED_HOME, + "ARMED_AWAY": STATE_ALARM_ARMED_AWAY, + "PENDING": STATE_ALARM_PENDING, +} + +# Legacy; to remove after YAML removal +CONF_CODE_DIGITS = "code_digits" +CONF_DEFAULT_LOCK_CODE = "default_lock_code" diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py new file mode 100644 index 00000000000..b118979f586 --- /dev/null +++ b/homeassistant/components/verisure/coordinator.py @@ -0,0 +1,114 @@ +"""DataUpdateCoordinator for the Verisure integration.""" +from __future__ import annotations + +from datetime import timedelta + +from verisure import ( + Error as VerisureError, + ResponseError as VerisureResponseError, + Session as Verisure, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, HTTP_SERVICE_UNAVAILABLE +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import Throttle + +from .const import CONF_GIID, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER + + +class VerisureDataUpdateCoordinator(DataUpdateCoordinator): + """A Verisure Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Verisure hub.""" + self.imageseries = {} + self.entry = entry + + self.verisure = Verisure( + username=entry.data[CONF_EMAIL], + password=entry.data[CONF_PASSWORD], + cookieFileName=hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}"), + ) + + super().__init__( + hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL + ) + + async def async_login(self) -> bool: + """Login to Verisure.""" + try: + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureError as ex: + LOGGER.error("Could not log in to verisure, %s", ex) + return False + + await self.hass.async_add_executor_job( + self.verisure.set_giid, self.entry.data[CONF_GIID] + ) + + return True + + async def async_logout(self, _event: Event) -> bool: + """Logout from Verisure.""" + try: + await self.hass.async_add_executor_job(self.verisure.logout) + except VerisureError as ex: + LOGGER.error("Could not log out from verisure, %s", ex) + return False + return True + + async def _async_update_data(self) -> dict: + """Fetch data from Verisure.""" + try: + overview = await self.hass.async_add_executor_job( + self.verisure.get_overview + ) + except VerisureResponseError as ex: + LOGGER.error("Could not read overview, %s", ex) + if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable + LOGGER.info("Trying to log in again") + await self.async_login() + return {} + raise + + # Store data in a way Home Assistant can easily consume it + return { + "alarm": overview["armState"], + "ethernet": overview.get("ethernetConnectedNow"), + "cameras": { + device["deviceLabel"]: device + for device in overview["customerImageCameras"] + }, + "climate": { + device["deviceLabel"]: device for device in overview["climateValues"] + }, + "door_window": { + device["deviceLabel"]: device + for device in overview["doorWindow"]["doorWindowDevice"] + }, + "locks": { + device["deviceLabel"]: device + for device in overview["doorLockStatusList"] + }, + "mice": { + device["deviceLabel"]: device + for device in overview["eventCounts"] + if device["deviceType"] == "MOUSE1" + }, + "smart_plugs": { + device["deviceLabel"]: device for device in overview["smartPlugs"] + }, + } + + @Throttle(timedelta(seconds=60)) + def update_smartcam_imageseries(self) -> None: + """Update the image series.""" + self.imageseries = self.verisure.get_camera_imageseries() + + @Throttle(timedelta(seconds=30)) + def smartcam_capture(self, device_id: str) -> None: + """Capture a new image from a smartcam.""" + self.verisure.capture_image(device_id) diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 228c8c6c176..eeec7e53a0a 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -1,137 +1,186 @@ """Support for Verisure locks.""" -from time import monotonic, sleep +from __future__ import annotations + +import asyncio +from typing import Any, Callable, Iterable + +from verisure import Error as VerisureError from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import current_platform +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import HUB as hub -from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, LOGGER +from .const import ( + CONF_GIID, + CONF_LOCK_CODE_DIGITS, + CONF_LOCK_DEFAULT_CODE, + DEFAULT_LOCK_CODE_DIGITS, + DOMAIN, + LOGGER, + SERVICE_DISABLE_AUTOLOCK, + SERVICE_ENABLE_AUTOLOCK, +) +from .coordinator import VerisureDataUpdateCoordinator -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure lock platform.""" - locks = [] - if int(hub.config.get(CONF_LOCKS, 1)): - hub.update_overview() - locks.extend( - [ - VerisureDoorlock(device_label) - for device_label in hub.get("$.doorLockStatusList[*].deviceLabel") - ] - ) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: + """Set up Verisure alarm control panel from a config entry.""" + coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - add_entities(locks) + platform = current_platform.get() + platform.async_register_entity_service( + SERVICE_DISABLE_AUTOLOCK, + {}, + VerisureDoorlock.disable_autolock.__name__, + ) + platform.async_register_entity_service( + SERVICE_ENABLE_AUTOLOCK, + {}, + VerisureDoorlock.enable_autolock.__name__, + ) + + async_add_entities( + VerisureDoorlock(coordinator, serial_number) + for serial_number in coordinator.data["locks"] + ) -class VerisureDoorlock(LockEntity): +class VerisureDoorlock(CoordinatorEntity, LockEntity): """Representation of a Verisure doorlock.""" - def __init__(self, device_label): - """Initialize the Verisure lock.""" - self._device_label = device_label - self._state = None - self._digits = hub.config.get(CONF_CODE_DIGITS) - self._changed_by = None - self._change_timestamp = 0 - self._default_lock_code = hub.config.get(CONF_DEFAULT_LOCK_CODE) + coordinator: VerisureDataUpdateCoordinator - @property - def name(self): - """Return the name of the lock.""" - return hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')].area", self._device_label + def __init__( + self, coordinator: VerisureDataUpdateCoordinator, serial_number: str + ) -> None: + """Initialize the Verisure lock.""" + super().__init__(coordinator) + self.serial_number = serial_number + self._state = None + self._digits = coordinator.entry.options.get( + CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS ) @property - def state(self): - """Return the state of the lock.""" - return self._state + def name(self) -> str: + """Return the name of this entity.""" + return self.coordinator.data["locks"][self.serial_number]["area"] @property - def available(self): + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self.serial_number + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + area = self.coordinator.data["locks"][self.serial_number]["area"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": "Lockguard Smartlock", + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } + + @property + def available(self) -> bool: """Return True if entity is available.""" return ( - hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')]", self._device_label - ) - is not None + super().available and self.serial_number in self.coordinator.data["locks"] ) @property - def changed_by(self): + def changed_by(self) -> str | None: """Last change triggered by.""" - return self._changed_by + return self.coordinator.data["locks"][self.serial_number].get("userString") @property - def code_format(self): + def code_format(self) -> str: """Return the required six digit code.""" return "^\\d{%s}$" % self._digits - def update(self): - """Update lock status.""" - if monotonic() - self._change_timestamp < 10: - return - hub.update_overview() - status = hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState", - self._device_label, - ) - if status == "UNLOCKED": - self._state = STATE_UNLOCKED - elif status == "LOCKED": - self._state = STATE_LOCKED - elif status != "PENDING": - LOGGER.error("Unknown lock state %s", status) - self._changed_by = hub.get_first( - "$.doorLockStatusList[?(@.deviceLabel=='%s')].userString", - self._device_label, - ) - @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" - return self._state == STATE_LOCKED + return ( + self.coordinator.data["locks"][self.serial_number]["lockedState"] + == "LOCKED" + ) - def unlock(self, **kwargs): + async def async_unlock(self, **kwargs) -> None: """Send unlock command.""" - if self._state is None: - return - - code = kwargs.get(ATTR_CODE, self._default_lock_code) + code = kwargs.get( + ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE) + ) if code is None: LOGGER.error("Code required but none provided") return - self.set_lock_state(code, STATE_UNLOCKED) + await self.async_set_lock_state(code, STATE_UNLOCKED) - def lock(self, **kwargs): + async def async_lock(self, **kwargs) -> None: """Send lock command.""" - if self._state == STATE_LOCKED: - return - - code = kwargs.get(ATTR_CODE, self._default_lock_code) + code = kwargs.get( + ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE) + ) if code is None: LOGGER.error("Code required but none provided") return - self.set_lock_state(code, STATE_LOCKED) + await self.async_set_lock_state(code, STATE_LOCKED) - def set_lock_state(self, code, state): + async def async_set_lock_state(self, code: str, state: str) -> None: """Send set lock state command.""" - lock_state = "lock" if state == STATE_LOCKED else "unlock" - transaction_id = hub.session.set_lock_state( - code, self._device_label, lock_state - )["doorLockStateChangeTransactionId"] + target_state = "lock" if state == STATE_LOCKED else "unlock" + lock_state = await self.hass.async_add_executor_job( + self.coordinator.verisure.set_lock_state, + code, + self.serial_number, + target_state, + ) + LOGGER.debug("Verisure doorlock %s", state) transaction = {} attempts = 0 while "result" not in transaction: - transaction = hub.session.get_lock_state_transaction(transaction_id) + transaction = await self.hass.async_add_executor_job( + self.coordinator.verisure.get_lock_state_transaction, + lock_state["doorLockStateChangeTransactionId"], + ) attempts += 1 if attempts == 30: break if attempts > 1: - sleep(0.5) + await asyncio.sleep(0.5) if transaction["result"] == "OK": self._state = state - self._change_timestamp = monotonic() + + def disable_autolock(self) -> None: + """Disable autolock on a doorlock.""" + try: + self.coordinator.verisure.set_lock_config( + self.serial_number, auto_lock_enabled=False + ) + LOGGER.debug("Disabling autolock on %s", self.serial_number) + except VerisureError as ex: + LOGGER.error("Could not disable autolock, %s", ex) + + def enable_autolock(self) -> None: + """Enable autolock on a doorlock.""" + try: + self.coordinator.verisure.set_lock_config( + self.serial_number, auto_lock_enabled=True + ) + LOGGER.debug("Enabling autolock on %s", self.serial_number) + except VerisureError as ex: + LOGGER.error("Could not enable autolock, %s", ex) diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 814b5f148fa..074ef4f955c 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,6 +2,8 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["jsonpath==0.82", "vsure==1.7.2"], - "codeowners": ["@frenck"] + "requirements": ["vsure==1.7.3"], + "codeowners": ["@frenck"], + "config_flow": true, + "dhcp": [{ "macaddress": "0023C1*" }] } diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index ac7c8f40e8d..93e1793da8d 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,178 +1,230 @@ """Support for Verisure sensors.""" +from __future__ import annotations + +from typing import Any, Callable, Iterable + +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + SensorEntity, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import HUB as hub -from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS +from .const import CONF_GIID, DEVICE_TYPE_NAME, DOMAIN +from .coordinator import VerisureDataUpdateCoordinator -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure platform.""" - sensors = [] - hub.update_overview() +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: + """Set up Verisure sensors based on a config entry.""" + coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - if int(hub.config.get(CONF_THERMOMETERS, 1)): - sensors.extend( - [ - VerisureThermometer(device_label) - for device_label in hub.get( - "$.climateValues[?(@.temperature)].deviceLabel" - ) - ] - ) + sensors: list[Entity] = [ + VerisureThermometer(coordinator, serial_number) + for serial_number, values in coordinator.data["climate"].items() + if "temperature" in values + ] - if int(hub.config.get(CONF_HYDROMETERS, 1)): - sensors.extend( - [ - VerisureHygrometer(device_label) - for device_label in hub.get( - "$.climateValues[?(@.humidity)].deviceLabel" - ) - ] - ) + sensors.extend( + VerisureHygrometer(coordinator, serial_number) + for serial_number, values in coordinator.data["climate"].items() + if "humidity" in values + ) - if int(hub.config.get(CONF_MOUSE, 1)): - sensors.extend( - [ - VerisureMouseDetection(device_label) - for device_label in hub.get( - "$.eventCounts[?(@.deviceType=='MOUSE1')].deviceLabel" - ) - ] - ) + sensors.extend( + VerisureMouseDetection(coordinator, serial_number) + for serial_number in coordinator.data["mice"] + ) - add_entities(sensors) + async_add_entities(sensors) -class VerisureThermometer(Entity): +class VerisureThermometer(CoordinatorEntity, SensorEntity): """Representation of a Verisure thermometer.""" - def __init__(self, device_label): + coordinator: VerisureDataUpdateCoordinator + + def __init__( + self, coordinator: VerisureDataUpdateCoordinator, serial_number: str + ) -> None: """Initialize the sensor.""" - self._device_label = device_label + super().__init__(coordinator) + self.serial_number = serial_number @property - def name(self): - """Return the name of the device.""" - return ( - hub.get_first( - "$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label - ) - + " temperature" + def name(self) -> str: + """Return the name of the entity.""" + name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] + return f"{name} Temperature" + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return f"{self.serial_number}_temperature" + + @property + def device_class(self) -> str: + """Return the class of this entity.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + device_type = self.coordinator.data["climate"][self.serial_number].get( + "deviceType" ) + area = self.coordinator.data["climate"][self.serial_number]["deviceArea"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": DEVICE_TYPE_NAME.get(device_type, device_type), + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } @property - def state(self): - """Return the state of the device.""" - return hub.get_first( - "$.climateValues[?(@.deviceLabel=='%s')].temperature", self._device_label - ) + def state(self) -> str | None: + """Return the state of the entity.""" + return self.coordinator.data["climate"][self.serial_number]["temperature"] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return ( - hub.get_first( - "$.climateValues[?(@.deviceLabel=='%s')].temperature", - self._device_label, - ) - is not None + super().available + and self.serial_number in self.coordinator.data["climate"] + and "temperature" in self.coordinator.data["climate"][self.serial_number] ) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return TEMP_CELSIUS - # pylint: disable=no-self-use - def update(self): - """Update the sensor.""" - hub.update_overview() - -class VerisureHygrometer(Entity): +class VerisureHygrometer(CoordinatorEntity, SensorEntity): """Representation of a Verisure hygrometer.""" - def __init__(self, device_label): + coordinator: VerisureDataUpdateCoordinator + + def __init__( + self, coordinator: VerisureDataUpdateCoordinator, serial_number: str + ) -> None: """Initialize the sensor.""" - self._device_label = device_label + super().__init__(coordinator) + self.serial_number = serial_number @property - def name(self): - """Return the name of the device.""" - return ( - hub.get_first( - "$.climateValues[?(@.deviceLabel=='%s')].deviceArea", self._device_label - ) - + " humidity" + def name(self) -> str: + """Return the name of the entity.""" + name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] + return f"{name} Humidity" + + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return f"{self.serial_number}_humidity" + + @property + def device_class(self) -> str: + """Return the class of this entity.""" + return DEVICE_CLASS_HUMIDITY + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + device_type = self.coordinator.data["climate"][self.serial_number].get( + "deviceType" ) + area = self.coordinator.data["climate"][self.serial_number]["deviceArea"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": DEVICE_TYPE_NAME.get(device_type, device_type), + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } @property - def state(self): - """Return the state of the device.""" - return hub.get_first( - "$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label - ) + def state(self) -> str | None: + """Return the state of the entity.""" + return self.coordinator.data["climate"][self.serial_number]["humidity"] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return ( - hub.get_first( - "$.climateValues[?(@.deviceLabel=='%s')].humidity", self._device_label - ) - is not None + super().available + and self.serial_number in self.coordinator.data["climate"] + and "humidity" in self.coordinator.data["climate"][self.serial_number] ) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return PERCENTAGE - # pylint: disable=no-self-use - def update(self): - """Update the sensor.""" - hub.update_overview() - -class VerisureMouseDetection(Entity): +class VerisureMouseDetection(CoordinatorEntity, SensorEntity): """Representation of a Verisure mouse detector.""" - def __init__(self, device_label): + coordinator: VerisureDataUpdateCoordinator + + def __init__( + self, coordinator: VerisureDataUpdateCoordinator, serial_number: str + ) -> None: """Initialize the sensor.""" - self._device_label = device_label + super().__init__(coordinator) + self.serial_number = serial_number @property - def name(self): - """Return the name of the device.""" - return ( - hub.get_first( - "$.eventCounts[?(@.deviceLabel=='%s')].area", self._device_label - ) - + " mouse" - ) + def name(self) -> str: + """Return the name of the entity.""" + name = self.coordinator.data["mice"][self.serial_number]["area"] + return f"{name} Mouse" @property - def state(self): - """Return the state of the device.""" - return hub.get_first( - "$.eventCounts[?(@.deviceLabel=='%s')].detections", self._device_label - ) + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return f"{self.serial_number}_mice" @property - def available(self): + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + area = self.coordinator.data["mice"][self.serial_number]["area"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": "Mouse detector", + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } + + @property + def state(self) -> str | None: + """Return the state of the entity.""" + return self.coordinator.data["mice"][self.serial_number]["detections"] + + @property + def available(self) -> bool: """Return True if entity is available.""" return ( - hub.get_first("$.eventCounts[?(@.deviceLabel=='%s')]", self._device_label) - is not None + super().available + and self.serial_number in self.coordinator.data["mice"] + and "detections" in self.coordinator.data["mice"][self.serial_number] ) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return "Mice" - - # pylint: disable=no-self-use - def update(self): - """Update the sensor.""" - hub.update_overview() diff --git a/homeassistant/components/verisure/services.yaml b/homeassistant/components/verisure/services.yaml index 885b8597549..2a4e2a008be 100644 --- a/homeassistant/components/verisure/services.yaml +++ b/homeassistant/components/verisure/services.yaml @@ -1,6 +1,23 @@ capture_smartcam: - description: Capture a new image from a smartcam. - fields: - device_serial: - description: The serial number of the smartcam you want to capture an image from. - example: 2DEU AT5Z + name: Capture SmartCam image + description: Capture a new image from a Verisure SmartCam + target: + entity: + integration: verisure + domain: camera + +enable_autolock: + name: Enable autolock + description: Enable autolock of a Verisure Lockguard Smartlock + target: + entity: + integration: verisure + domain: lock + +disable_autolock: + name: Disable autolock + description: Disable autolock of a Verisure Lockguard Smartlock + target: + entity: + integration: verisure + domain: lock diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json new file mode 100644 index 00000000000..5170bff5faa --- /dev/null +++ b/homeassistant/components/verisure/strings.json @@ -0,0 +1,47 @@ +{ + "config": { + "step": { + "user": { + "data": { + "description": "Sign-in with your Verisure My Pages account.", + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "installation": { + "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant.", + "data": { + "giid": "Installation" + } + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Verisure My Pages account.", + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "lock_code_digits": "Number of digits in PIN code for locks", + "lock_default_code": "Default PIN code for locks, used if none is given" + } + } + }, + "error": { + "code_format_mismatch": "The default PIN code does not match the required number of digits" + } + } +} diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 4615e0a2a49..f55db8ce428 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,78 +1,96 @@ """Support for Verisure Smartplugs.""" +from __future__ import annotations + from time import monotonic +from typing import Any, Callable, Iterable from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CONF_SMARTPLUGS, HUB as hub +from .const import CONF_GIID, DOMAIN +from .coordinator import VerisureDataUpdateCoordinator -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Verisure switch platform.""" - if not int(hub.config.get(CONF_SMARTPLUGS, 1)): - return False - - hub.update_overview() - switches = [] - switches.extend( - [ - VerisureSmartplug(device_label) - for device_label in hub.get("$.smartPlugs[*].deviceLabel") - ] +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[Iterable[Entity]], None], +) -> None: + """Set up Verisure alarm control panel from a config entry.""" + coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + VerisureSmartplug(coordinator, serial_number) + for serial_number in coordinator.data["smart_plugs"] ) - add_entities(switches) -class VerisureSmartplug(SwitchEntity): +class VerisureSmartplug(CoordinatorEntity, SwitchEntity): """Representation of a Verisure smartplug.""" - def __init__(self, device_id): + coordinator: VerisureDataUpdateCoordinator + + def __init__( + self, coordinator: VerisureDataUpdateCoordinator, serial_number: str + ) -> None: """Initialize the Verisure device.""" - self._device_label = device_id + super().__init__(coordinator) + self.serial_number = serial_number self._change_timestamp = 0 self._state = False @property - def name(self): - """Return the name or location of the smartplug.""" - return hub.get_first( - "$.smartPlugs[?(@.deviceLabel == '%s')].area", self._device_label - ) + def name(self) -> str: + """Return the name of this entity.""" + return self.coordinator.data["smart_plugs"][self.serial_number]["area"] @property - def is_on(self): + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self.serial_number + + @property + def device_info(self) -> dict[str, Any]: + """Return device information about this entity.""" + area = self.coordinator.data["smart_plugs"][self.serial_number]["area"] + return { + "name": area, + "suggested_area": area, + "manufacturer": "Verisure", + "model": "SmartPlug", + "identifiers": {(DOMAIN, self.serial_number)}, + "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), + } + + @property + def is_on(self) -> bool: """Return true if on.""" if monotonic() - self._change_timestamp < 10: return self._state self._state = ( - hub.get_first( - "$.smartPlugs[?(@.deviceLabel == '%s')].currentState", - self._device_label, - ) + self.coordinator.data["smart_plugs"][self.serial_number]["currentState"] == "ON" ) return self._state @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return ( - hub.get_first("$.smartPlugs[?(@.deviceLabel == '%s')]", self._device_label) - is not None + super().available + and self.serial_number in self.coordinator.data["smart_plugs"] ) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs) -> None: """Set smartplug status on.""" - hub.session.set_smartplug_state(self._device_label, True) + self.coordinator.verisure.set_smartplug_state(self.serial_number, True) self._state = True self._change_timestamp = monotonic() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs) -> None: """Set smartplug status off.""" - hub.session.set_smartplug_state(self._device_label, False) + self.coordinator.verisure.set_smartplug_state(self.serial_number, False) self._state = False self._change_timestamp = monotonic() - - # pylint: disable=no-self-use - def update(self): - """Get the latest date of the smartplug.""" - hub.update_overview() diff --git a/homeassistant/components/verisure/translations/ca.json b/homeassistant/components/verisure/translations/ca.json new file mode 100644 index 00000000000..0ddcf9513f4 --- /dev/null +++ b/homeassistant/components/verisure/translations/ca.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "installation": { + "data": { + "giid": "Instal\u00b7laci\u00f3" + }, + "description": "Home Assistant ha trobat diverses instal\u00b7lacions Verisure al compte de My Pages. Selecciona la instal\u00b7laci\u00f3 a afegir a Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Torna a autenticar-te amb el compte de Verisure My Pages.", + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + } + }, + "user": { + "data": { + "description": "Inicia sessi\u00f3 amb el compte de Verisure My Pages.", + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "El codi PIN predeterminat no coincideix amb el nombre de d\u00edgits correcte" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Nombre de d\u00edgits del codi PIN dels panys", + "lock_default_code": "Codi PIN dels panys predeterminat, s'utilitza si no se n'indica cap" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/cs.json b/homeassistant/components/verisure/translations/cs.json new file mode 100644 index 00000000000..34165a2381f --- /dev/null +++ b/homeassistant/components/verisure/translations/cs.json @@ -0,0 +1,26 @@ +{ + "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": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + }, + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json new file mode 100644 index 00000000000..f9edd2e7b16 --- /dev/null +++ b/homeassistant/components/verisure/translations/de.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "installation": { + "data": { + "giid": "Installation" + } + }, + "reauth_confirm": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + }, + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Der Standard-PIN-Code stimmt nicht mit der erforderlichen Anzahl von Ziffern \u00fcberein" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Anzahl der Ziffern im PIN-Code f\u00fcr Schl\u00f6sser", + "lock_default_code": "Standard-PIN-Code f\u00fcr Schl\u00f6sser, wird verwendet wenn keiner angegeben wird" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/el.json b/homeassistant/components/verisure/translations/el.json new file mode 100644 index 00000000000..87313dba1d4 --- /dev/null +++ b/homeassistant/components/verisure/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "email": "\u0397\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03a4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json new file mode 100644 index 00000000000..57f73c3772b --- /dev/null +++ b/homeassistant/components/verisure/translations/en.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "installation": { + "data": { + "giid": "Installation" + }, + "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Verisure My Pages account.", + "email": "Email", + "password": "Password" + } + }, + "user": { + "data": { + "description": "Sign-in with your Verisure My Pages account.", + "email": "Email", + "password": "Password" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "The default PIN code does not match the required number of digits" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Number of digits in PIN code for locks", + "lock_default_code": "Default PIN code for locks, used if none is given" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json new file mode 100644 index 00000000000..38605e4f86b --- /dev/null +++ b/homeassistant/components/verisure/translations/es.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "installation": { + "data": { + "giid": "Instalaci\u00f3n" + }, + "description": "Home Assistant encontr\u00f3 varias instalaciones de Verisure en su cuenta de Mis p\u00e1ginas. Por favor, seleccione la instalaci\u00f3n para agregar a Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Vuelva a autenticarse con su cuenta Verisure My Pages." + } + }, + "user": { + "data": { + "description": "Inicia sesi\u00f3n con tu cuenta Verisure My Pages." + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "El c\u00f3digo PIN predeterminado no coincide con el n\u00famero necesario de d\u00edgitos" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "N\u00famero de d\u00edgitos del c\u00f3digo PIN de las cerraduras", + "lock_default_code": "C\u00f3digo PIN por defecto para las cerraduras, utilizado si no se indica ninguno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/et.json b/homeassistant/components/verisure/translations/et.json new file mode 100644 index 00000000000..78a2c987ef2 --- /dev/null +++ b/homeassistant/components/verisure/translations/et.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "installation": { + "data": { + "giid": "Paigaldus" + }, + "description": "Home Assistant leidis kontolt Minu lehed mitu Verisure paigaldust. Vali Home Assistantile lisatav paigaldus." + }, + "reauth_confirm": { + "data": { + "description": "Taastuvasta oma Verisure My Pages'i kontoga.", + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + }, + "user": { + "data": { + "description": "Logi sisse oma Verisure My Pages kontoga.", + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Vaikimisi PIN-koodi numbrite arv on vale" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Lukkude PIN-koodi numbrite arv", + "lock_default_code": "Lukkude VAIKIMISI PIN-kood, mida kasutatakse juhul kui seda polem\u00e4\u00e4ratud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json new file mode 100644 index 00000000000..4909fec8894 --- /dev/null +++ b/homeassistant/components/verisure/translations/fr.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "installation": { + "data": { + "giid": "Installation" + }, + "description": "Home Assistant a trouv\u00e9 plusieurs installations Verisure dans votre compte My Pages. Veuillez s\u00e9lectionner l'installation \u00e0 ajouter \u00e0 Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.", + "email": "Email", + "password": "Mot de passe" + } + }, + "user": { + "data": { + "description": "Connectez-vous avec votre compte Verisure My Pages.", + "email": "Email", + "password": "Mot de passe" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Le code NIP par d\u00e9faut ne correspond pas au nombre de chiffres requis" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Nombre de chiffres du code NIP pour les serrures", + "lock_default_code": "Code PIN par d\u00e9faut pour les serrures, utilis\u00e9 si aucun n'est indiqu\u00e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json new file mode 100644 index 00000000000..85e53003566 --- /dev/null +++ b/homeassistant/components/verisure/translations/hu.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "installation": { + "data": { + "giid": "Telep\u00edt\u00e9s" + } + }, + "reauth_confirm": { + "data": { + "description": "Hiteles\u00edts \u00fajra a Verisure My Pages fi\u00f3koddal.", + "email": "E-mail", + "password": "Jelsz\u00f3" + } + }, + "user": { + "data": { + "description": "Jelentkezz be a Verisure My Pages fi\u00f3koddal.", + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Az alap\u00e9rtelmezett PIN-k\u00f3d nem egyezik meg a sz\u00fcks\u00e9ges sz\u00e1mjegyek sz\u00e1m\u00e1val" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Sz\u00e1mjegyek sz\u00e1ma a z\u00e1rak PIN-k\u00f3dj\u00e1ban", + "lock_default_code": "Alap\u00e9rtelmezett PIN-k\u00f3d z\u00e1rakhoz, akkor haszn\u00e1latos, ha nincs m\u00e1s megadva" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json new file mode 100644 index 00000000000..1c90871db71 --- /dev/null +++ b/homeassistant/components/verisure/translations/it.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "installation": { + "data": { + "giid": "Installazione" + }, + "description": "Home Assistant ha trovato pi\u00f9 installazioni Verisure nel tuo account My Pages. Per favore, seleziona l'installazione da aggiungere a Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Autenticati nuovamente con il tuo account Verisure My Pages.", + "email": "E-mail", + "password": "Password" + } + }, + "user": { + "data": { + "description": "Accedi con il tuo account Verisure My Pages.", + "email": "E-mail", + "password": "Password" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Il codice PIN predefinito non corrisponde al numero di cifre richiesto" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Numero di cifre nel codice PIN per le serrature", + "lock_default_code": "Codice PIN predefinito per le serrature, utilizzato se non ne viene fornito nessuno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ko.json b/homeassistant/components/verisure/translations/ko.json new file mode 100644 index 00000000000..470aed32b19 --- /dev/null +++ b/homeassistant/components/verisure/translations/ko.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "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": { + "installation": { + "data": { + "giid": "\uc124\uce58" + }, + "description": "Home Assistant\uac00 My Pages \uacc4\uc815\uc5d0\uc11c \uc5ec\ub7ec \uac00\uc9c0 Verisure \uc124\uce58\ub97c \ubc1c\uacac\ud588\uc2b5\ub2c8\ub2e4. Home Assistant\uc5d0 \ucd94\uac00\ud560 \uc124\uce58\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + }, + "reauth_confirm": { + "data": { + "description": "Verisure My Pages \uacc4\uc815\uc73c\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc8fc\uc138\uc694.", + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + } + }, + "user": { + "data": { + "description": "Verisure My Pages \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778\ud558\uae30.", + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "\uae30\ubcf8 PIN \ucf54\ub4dc\uac00 \ud544\uc694\ud55c \uc790\ub9bf\uc218\uc640 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "\uc7a0\uae08\uc7a5\uce58\uc6a9 PIN \ucf54\ub4dc\uc758 \uc790\ub9bf\uc218", + "lock_default_code": "\uc7a0\uae08\uc7a5\uce58\uc5d0 \ub300\ud55c \uae30\ubcf8 PIN \ucf54\ub4dc (\uc81c\uacf5\ub41c PIN\uc774 \uc5c6\ub294 \uacbd\uc6b0 \uc0ac\uc6a9)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json new file mode 100644 index 00000000000..d0519e584fd --- /dev/null +++ b/homeassistant/components/verisure/translations/nl.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "installation": { + "data": { + "giid": "Installatie" + }, + "description": "Home Assistant heeft meerdere Verisure-installaties gevonden in uw My Pages-account. Selecteer de installatie om toe te voegen aan Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Verifieer opnieuw met uw Verisure My Pages-account.", + "email": "E-mail", + "password": "Wachtwoord" + } + }, + "user": { + "data": { + "description": "Aanmelden met Verisure My Pages-account.", + "email": "E-mail", + "password": "Wachtwoord" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "De standaard pincode komt niet overeen met het vereiste aantal cijfers" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Aantal cijfers in pincode voor vergrendelingen", + "lock_default_code": "Standaard pincode voor vergrendelingen, gebruikt als er geen wordt gegeven" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/no.json b/homeassistant/components/verisure/translations/no.json new file mode 100644 index 00000000000..0bd5529c51d --- /dev/null +++ b/homeassistant/components/verisure/translations/no.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "installation": { + "data": { + "giid": "Installasjon" + }, + "description": "Home Assistant fant flere Verisure-installasjoner i Mine sider-kontoen din. Velg installasjonen du vil legge til i Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "Autentiser p\u00e5 nytt med Verisure Mine sider-kontoen din.", + "email": "E-post", + "password": "Passord" + } + }, + "user": { + "data": { + "description": "Logg p\u00e5 med Verisure Mine sider-kontoen din.", + "email": "E-post", + "password": "Passord" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Standard PIN-kode samsvarer ikke med det n\u00f8dvendige antallet sifre" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Antall sifre i PIN-kode for l\u00e5ser", + "lock_default_code": "Standard PIN-kode for l\u00e5ser, brukes hvis ingen er oppgitt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/pl.json b/homeassistant/components/verisure/translations/pl.json new file mode 100644 index 00000000000..baaf61f60c3 --- /dev/null +++ b/homeassistant/components/verisure/translations/pl.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "installation": { + "data": { + "giid": "Instalacja" + }, + "description": "Home Assistant znalaz\u0142 wiele instalacji Verisure na Twoim koncie. Wybierz instalacj\u0119, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta." + }, + "reauth_confirm": { + "data": { + "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Verisure.", + "email": "Adres e-mail", + "password": "Has\u0142o" + } + }, + "user": { + "data": { + "description": "Zaloguj si\u0119 na swoje konto Verisure.", + "email": "Adres e-mail", + "password": "Has\u0142o" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Domy\u015blny kod PIN nie odpowiada wymaganej liczbie cyfr" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Liczba cyfr w kodzie PIN dla zamk\u00f3w", + "lock_default_code": "Domy\u015blny kod PIN dla zamk\u00f3w. U\u017cywany, je\u015bli nie podano \u017cadnego." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/pt.json b/homeassistant/components/verisure/translations/pt.json new file mode 100644 index 00000000000..a60db2b4ea0 --- /dev/null +++ b/homeassistant/components/verisure/translations/pt.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ru.json b/homeassistant/components/verisure/translations/ru.json new file mode 100644 index 00000000000..430b8d773d0 --- /dev/null +++ b/homeassistant/components/verisure/translations/ru.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "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": { + "installation": { + "data": { + "giid": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430" + }, + "description": "Home Assistant \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043e\u043a Verisure \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041c\u043e\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b\u00bb \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant." + }, + "reauth_confirm": { + "data": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "user": { + "data": { + "description": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "PIN-\u043a\u043e\u0434 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u043e\u043c\u0443 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0443 \u0446\u0438\u0444\u0440." + }, + "step": { + "init": { + "data": { + "lock_code_digits": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0446\u0438\u0444\u0440 \u0432 PIN-\u043a\u043e\u0434\u0435 \u0434\u043b\u044f \u0437\u0430\u043c\u043a\u043e\u0432", + "lock_default_code": "PIN-\u043a\u043e\u0434 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0437\u0430\u043c\u043a\u043e\u0432, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439, \u0435\u0441\u043b\u0438 \u043d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/zh-Hant.json b/homeassistant/components/verisure/translations/zh-Hant.json new file mode 100644 index 00000000000..29410e390fe --- /dev/null +++ b/homeassistant/components/verisure/translations/zh-Hant.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "installation": { + "data": { + "giid": "\u5b89\u88dd" + }, + "description": "Home Assistant \u65bc My Pages \u5e33\u865f\u4e2d\u627e\u5230\u591a\u500b Verisure \u5b89\u88dd\u3002\u8acb\u9078\u64c7\u6240\u8981\u65b0\u589e\u81f3 Home Assistant \u7684\u9805\u76ee\u3002" + }, + "reauth_confirm": { + "data": { + "description": "\u91cd\u65b0\u8a8d\u8b49 Verisure My Pages \u5e33\u865f\u3002", + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + } + }, + "user": { + "data": { + "description": "\u4ee5 Verisure My Pages \u5e33\u865f\u767b\u5165", + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "\u9810\u8a2d PIN \u78bc\u8207\u6240\u9700\u6578\u5b57\u6578\u4e0d\u7b26\u5408" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "\u9580\u9396 PIN \u78bc\u6578\u5b57\u6578", + "lock_default_code": "\u9810\u8a2d\u9580\u9396 PIN \u78bc\uff0c\u65bc\u672a\u63d0\u4f9b\u6642\u4f7f\u7528" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index e598093cd37..d29032af399 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -1,7 +1,7 @@ """Support for VersaSense sensor peripheral.""" import logging -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from . import DOMAIN from .const import ( @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(sensor_list) -class VSensor(Entity): +class VSensor(SensorEntity): """Representation of a Sensor.""" def __init__(self, peripheral, parent_name, unit, measurement, consumer): diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 7fc1a097d81..7f55273383d 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.4.2"], + "requirements": ["pyhaversion==21.3.0"], "codeowners": ["@fabaff", "@ludeeus"], "quality_scale": "internal" } diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index c030383a4a6..9d558f4ba7c 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,41 +1,42 @@ """Sensor that can display the current Home Assistant versions.""" from datetime import timedelta -from pyhaversion import ( - DockerVersion, - HaIoVersion, - HassioVersion, - LocalVersion, - PyPiVersion, -) +from pyhaversion import HaVersion, HaVersionChannel, HaVersionSource import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_SOURCE from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle ALL_IMAGES = [ "default", "intel-nuc", - "qemux86", - "qemux86-64", - "qemuarm", - "qemuarm-64", - "raspberrypi", - "raspberrypi2", - "raspberrypi3", - "raspberrypi3-64", - "raspberrypi4", - "raspberrypi4-64", - "tinker", "odroid-c2", "odroid-n2", "odroid-xu", + "qemuarm-64", + "qemuarm", + "qemux86-64", + "qemux86", + "raspberrypi", + "raspberrypi2", + "raspberrypi3-64", + "raspberrypi3", + "raspberrypi4-64", + "raspberrypi4", + "tinker", +] +ALL_SOURCES = [ + "container", + "haio", + "local", + "pypi", + "supervisor", + "hassio", # Kept to not break existing configurations + "docker", # Kept to not break existing configurations ] -ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"] CONF_BETA = "beta" CONF_IMAGE = "image" @@ -69,21 +70,30 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= session = async_get_clientsession(hass) - if beta: - branch = "beta" - else: - branch = "stable" + channel = HaVersionChannel.BETA if beta else HaVersionChannel.STABLE if source == "pypi": - haversion = VersionData(PyPiVersion(hass.loop, session, branch)) - elif source == "hassio": - haversion = VersionData(HassioVersion(hass.loop, session, branch, image)) - elif source == "docker": - haversion = VersionData(DockerVersion(hass.loop, session, branch, image)) + haversion = VersionData( + HaVersion(session, source=HaVersionSource.PYPI, channel=channel) + ) + elif source in ["hassio", "supervisor"]: + haversion = VersionData( + HaVersion( + session, source=HaVersionSource.SUPERVISOR, channel=channel, image=image + ) + ) + elif source in ["docker", "container"]: + if image is not None and image != DEFAULT_IMAGE: + image = f"{image}-homeassistant" + haversion = VersionData( + HaVersion( + session, source=HaVersionSource.CONTAINER, channel=channel, image=image + ) + ) elif source == "haio": - haversion = VersionData(HaIoVersion(hass.loop, session)) + haversion = VersionData(HaVersion(session, source=HaVersionSource.HAIO)) else: - haversion = VersionData(LocalVersion(hass.loop, session)) + haversion = VersionData(HaVersion(session, source=HaVersionSource.LOCAL)) if not name: if source == DEFAULT_SOURCE: @@ -94,18 +104,31 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([VersionSensor(haversion, name)], True) -class VersionSensor(Entity): +class VersionData: + """Get the latest data and update the states.""" + + def __init__(self, api: HaVersion): + """Initialize the data object.""" + self.api = api + + @Throttle(TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest version information.""" + await self.api.get_version() + + +class VersionSensor(SensorEntity): """Representation of a Home Assistant version sensor.""" - def __init__(self, haversion, name): + def __init__(self, data: VersionData, name: str): """Initialize the Version sensor.""" - self.haversion = haversion + self.data = data self._name = name self._state = None async def async_update(self): """Get the latest version information.""" - await self.haversion.async_update() + await self.data.async_update() @property def name(self): @@ -115,27 +138,14 @@ class VersionSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.haversion.api.version + return self.data.api.version @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return attributes for the sensor.""" - return self.haversion.api.version_data + return self.data.api.version_data @property def icon(self): """Return the icon to use in the frontend, if any.""" return ICON - - -class VersionData: - """Get the latest data and update the states.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - - @Throttle(TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest version information.""" - await self.api.get_version() diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 24bd0f000df..686a71427c3 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -156,8 +156,8 @@ async def async_unload_entry(hass, entry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 1d1320d8d78..d01d3d4dc5d 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -101,7 +101,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): return self.smartfan.uuid @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the fan.""" return { "mode": self.smartfan.mode, diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 53dfdc5f0a9..b98c87e5a7f 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -33,23 +33,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DOMAIN][VS_DISPATCHERS].append(disp) _async_setup_entities(hass.data[DOMAIN][VS_LIGHTS], async_add_entities) - return True @callback def _async_setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" - dev_list = [] + entities = [] for dev in devices: if DEV_TYPE_TO_HA.get(dev.device_type) == "light": - dev_list.append(VeSyncDimmerHA(dev)) + entities.append(VeSyncDimmerHA(dev)) else: _LOGGER.debug( "%s - Unknown device type - %s", dev.device_name, dev.device_type ) continue - async_add_entities(dev_list, update_before_add=True) + async_add_entities(entities, update_before_add=True) class VeSyncDimmerHA(VeSyncDevice, LightEntity): diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 667cb16d128..6aa7a5774fd 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -8,7 +8,7 @@ "@thegardenmonkey" ], "requirements": [ - "pyvesync==1.2.0" + "pyvesync==1.3.1" ], "config_flow": true -} \ No newline at end of file +} diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 0ce4b931def..1d01e340b20 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -72,7 +72,7 @@ class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): self.smartplug = plug @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" if not hasattr(self.smartplug, "weekly_energy_total"): return {} diff --git a/homeassistant/components/vesync/translations/he.json b/homeassistant/components/vesync/translations/he.json new file mode 100644 index 00000000000..6f4191da70d --- /dev/null +++ b/homeassistant/components/vesync/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/hu.json b/homeassistant/components/vesync/translations/hu.json index 10607c5a136..91956aff452 100644 --- a/homeassistant/components/vesync/translations/hu.json +++ b/homeassistant/components/vesync/translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vesync/translations/id.json b/homeassistant/components/vesync/translations/id.json new file mode 100644 index 00000000000..968f8541849 --- /dev/null +++ b/homeassistant/components/vesync/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Masukkan Nama Pengguna dan Kata Sandi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/ko.json b/homeassistant/components/vesync/translations/ko.json index d11e9f9459d..a3f1fc1316a 100644 --- a/homeassistant/components/vesync/translations/ko.json +++ b/homeassistant/components/vesync/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -12,7 +12,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c" }, - "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud558\uae30" + "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" } } } diff --git a/homeassistant/components/vesync/translations/nl.json b/homeassistant/components/vesync/translations/nl.json index 36c7f315bcc..ab330237afd 100644 --- a/homeassistant/components/vesync/translations/nl.json +++ b/homeassistant/components/vesync/translations/nl.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "E-mailadres" + "username": "E-mail" }, "title": "Voer gebruikersnaam en wachtwoord in" } diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 5de968f5eac..10821859f9a 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -6,10 +6,9 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, HTTP_OK, TIME_MINUTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -82,7 +81,7 @@ async def async_http_request(hass, uri): _LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint") -class ViaggiaTrenoSensor(Entity): +class ViaggiaTrenoSensor(SensorEntity): """Implementation of a ViaggiaTreno sensor.""" def __init__(self, train_id, station_id, name): @@ -119,7 +118,7 @@ class ViaggiaTrenoSensor(Entity): return self._unit @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return extra attributes.""" self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return self._attributes diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 2b1a367215b..88c4ce33a86 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -3,6 +3,7 @@ import enum import logging from PyViCare.PyViCareDevice import Device +from PyViCare.PyViCareFuelCell import FuelCell from PyViCare.PyViCareGazBoiler import GazBoiler from PyViCare.PyViCareHeatPump import HeatPump import voluptuous as vol @@ -19,7 +20,7 @@ from homeassistant.helpers.storage import STORAGE_DIR _LOGGER = logging.getLogger(__name__) -VICARE_PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] +PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] DOMAIN = "vicare" PYVICARE_ERROR = "error" @@ -38,6 +39,7 @@ class HeatingType(enum.Enum): generic = "generic" gas = "gas" heatpump = "heatpump" + fuelcell = "fuelcell" CONFIG_SCHEMA = vol.Schema( @@ -77,6 +79,8 @@ def setup(hass, config): vicare_api = GazBoiler(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) elif heating_type == HeatingType.heatpump: vicare_api = HeatPump(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + elif heating_type == HeatingType.fuelcell: + vicare_api = FuelCell(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) else: vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) except AttributeError: @@ -90,7 +94,7 @@ def setup(hass, config): hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] hass.data[DOMAIN][VICARE_HEATING_TYPE] = heating_type - for platform in VICARE_PLATFORMS: + for platform in PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index b7e926b2379..823c4f1ba1b 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -85,6 +85,7 @@ SENSORS_BY_HEATINGTYPE = { SENSOR_HEATINGROD_LEVEL2, SENSOR_HEATINGROD_LEVEL3, ], + HeatingType.fuelcell: [SENSOR_BURNER_ACTIVE], } diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index d1accd8ea0a..c819c6593a1 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -111,7 +111,7 @@ async def async_setup_platform( { vol.Required(SERVICE_SET_VICARE_MODE_ATTR_MODE): vol.In( VICARE_TO_HA_HVAC_HEATING - ), + ) }, "set_vicare_mode", ) @@ -278,7 +278,7 @@ class ViCareClimate(ClimateEntity): self._api.activateProgram(vicare_program) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Show Device Attributes.""" return self._attributes diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index a14e00923c2..c988b2a4086 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -3,18 +3,20 @@ import logging import requests +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ICON, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, PERCENTAGE, TEMP_CELSIUS, TIME_HOURS, ) -from homeassistant.helpers.entity import Entity from . import ( DOMAIN as VICARE_DOMAIN, @@ -59,6 +61,13 @@ SENSOR_COMPRESSOR_HOURS_LOADCLASS3 = "compressor_hours_loadclass3" SENSOR_COMPRESSOR_HOURS_LOADCLASS4 = "compressor_hours_loadclass4" SENSOR_COMPRESSOR_HOURS_LOADCLASS5 = "compressor_hours_loadclass5" +# fuelcell sensors +SENSOR_POWER_PRODUCTION_CURRENT = "power_production_current" +SENSOR_POWER_PRODUCTION_TODAY = "power_production_today" +SENSOR_POWER_PRODUCTION_THIS_WEEK = "power_production_this_week" +SENSOR_POWER_PRODUCTION_THIS_MONTH = "power_production_this_month" +SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year" + SENSOR_TYPES = { SENSOR_OUTSIDE_TEMPERATURE: { CONF_NAME: "Outside Temperature", @@ -216,6 +225,42 @@ SENSOR_TYPES = { CONF_GETTER: lambda api: api.getReturnTemperature(), CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, }, + # fuelcell sensors + SENSOR_POWER_PRODUCTION_CURRENT: { + CONF_NAME: "Power production current", + CONF_ICON: None, + CONF_UNIT_OF_MEASUREMENT: ENERGY_WATT_HOUR, + CONF_GETTER: lambda api: api.getPowerProductionCurrent(), + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + SENSOR_POWER_PRODUCTION_TODAY: { + CONF_NAME: "Power production today", + CONF_ICON: None, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + CONF_GETTER: lambda api: api.getPowerProductionToday(), + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + SENSOR_POWER_PRODUCTION_THIS_WEEK: { + CONF_NAME: "Power production this week", + CONF_ICON: None, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + CONF_GETTER: lambda api: api.getPowerProductionThisWeek(), + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + SENSOR_POWER_PRODUCTION_THIS_MONTH: { + CONF_NAME: "Power production this month", + CONF_ICON: None, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + CONF_GETTER: lambda api: api.getPowerProductionThisMonth(), + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, + SENSOR_POWER_PRODUCTION_THIS_YEAR: { + CONF_NAME: "Power production this year", + CONF_ICON: None, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + CONF_GETTER: lambda api: api.getPowerProductionThisYear(), + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + }, } SENSORS_GENERIC = [SENSOR_OUTSIDE_TEMPERATURE, SENSOR_SUPPLY_TEMPERATURE] @@ -245,6 +290,27 @@ SENSORS_BY_HEATINGTYPE = { SENSOR_COMPRESSOR_HOURS_LOADCLASS5, SENSOR_RETURN_TEMPERATURE, ], + HeatingType.fuelcell: [ + # gas + SENSOR_BOILER_TEMPERATURE, + SENSOR_BURNER_HOURS, + SENSOR_BURNER_MODULATION, + SENSOR_BURNER_STARTS, + SENSOR_DHW_GAS_CONSUMPTION_TODAY, + SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, + SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, + SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, + SENSOR_GAS_CONSUMPTION_TODAY, + SENSOR_GAS_CONSUMPTION_THIS_WEEK, + SENSOR_GAS_CONSUMPTION_THIS_MONTH, + SENSOR_GAS_CONSUMPTION_THIS_YEAR, + # fuel cell + SENSOR_POWER_PRODUCTION_CURRENT, + SENSOR_POWER_PRODUCTION_TODAY, + SENSOR_POWER_PRODUCTION_THIS_WEEK, + SENSOR_POWER_PRODUCTION_THIS_MONTH, + SENSOR_POWER_PRODUCTION_THIS_YEAR, + ], } @@ -269,7 +335,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ViCareSensor(Entity): +class ViCareSensor(SensorEntity): """Representation of a ViCare sensor.""" def __init__(self, name, api, sensor_type): diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py index 2b9df3d9195..d5646c8caf3 100644 --- a/homeassistant/components/vilfo/config_flow.py +++ b/homeassistant/components/vilfo/config_flow.py @@ -13,8 +13,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC -from .const import DOMAIN # pylint:disable=unused-import -from .const import ROUTER_DEFAULT_HOST +from .const import DOMAIN, ROUTER_DEFAULT_HOST _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py index 74eb813bcc5..d47e738a858 100644 --- a/homeassistant/components/vilfo/const.py +++ b/homeassistant/components/vilfo/const.py @@ -1,13 +1,16 @@ """Constants for the Vilfo Router integration.""" -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, +) DOMAIN = "vilfo" ATTR_API_DATA_FIELD = "api_data_field" ATTR_API_DATA_FIELD_LOAD = "load" ATTR_API_DATA_FIELD_BOOT_TIME = "boot_time" -ATTR_DEVICE_CLASS = "device_class" -ATTR_ICON = "icon" ATTR_LABEL = "label" ATTR_LOAD = "load" ATTR_UNIT = "unit" diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index e2909647c2d..90527c60458 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -1,10 +1,10 @@ """Support for Vilfo Router sensors.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ATTR_ICON from .const import ( ATTR_API_DATA_FIELD, ATTR_DEVICE_CLASS, - ATTR_ICON, ATTR_LABEL, ATTR_UNIT, DOMAIN, @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, True) -class VilfoRouterSensor(Entity): +class VilfoRouterSensor(SensorEntity): """Define a Vilfo Router Sensor.""" def __init__(self, sensor_type, api): diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json index a75149507fc..34db9cf7cc9 100644 --- a/homeassistant/components/vilfo/translations/hu.json +++ b/homeassistant/components/vilfo/translations/hu.json @@ -1,5 +1,13 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vilfo/translations/id.json b/homeassistant/components/vilfo/translations/id.json new file mode 100644 index 00000000000..3871670a1cd --- /dev/null +++ b/homeassistant/components/vilfo/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" + }, + "step": { + "user": { + "data": { + "access_token": "Token Akses", + "host": "Host" + }, + "description": "Siapkan integrasi Router Vilfo. Anda memerlukan nama host/IP Router Vilfo dan token akses API. Untuk informasi lebih lanjut tentang integrasi ini dan cara mendapatkan data tersebut, kunjungi: https://www.home-assistant.io/integrations/vilfo", + "title": "Hubungkan ke Router Vilfo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/ko.json b/homeassistant/components/vilfo/translations/ko.json index 4b79130c750..980ee36d7a5 100644 --- a/homeassistant/components/vilfo/translations/ko.json +++ b/homeassistant/components/vilfo/translations/ko.json @@ -14,7 +14,7 @@ "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", "host": "\ud638\uc2a4\ud2b8" }, - "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 / IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984/IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "Vilfo \ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/vilfo/translations/nl.json b/homeassistant/components/vilfo/translations/nl.json index d4b117e2b70..304871cc145 100644 --- a/homeassistant/components/vilfo/translations/nl.json +++ b/homeassistant/components/vilfo/translations/nl.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "Deze Vilfo Router is al geconfigureerd." + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden. Controleer de door u verstrekte informatie en probeer het opnieuw.", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", - "unknown": "Er is een onverwachte fout opgetreden tijdens het instellen van de integratie." + "unknown": "Onverwachte fout" }, "step": { "user": { "data": { - "access_token": "Toegangstoken voor de Vilfo Router API", - "host": "Router hostnaam of IP-adres" + "access_token": "Toegangstoken", + "host": "Host" }, "description": "Stel de Vilfo Router-integratie in. U heeft de hostnaam/IP van uw Vilfo Router en een API-toegangstoken nodig. Voor meer informatie over deze integratie en hoe u die details kunt verkrijgen, gaat u naar: https://www.home-assistant.io/integrations/vilfo", "title": "Maak verbinding met de Vilfo Router" diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index a7a9c404f74..3719ada27ae 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,8 +1,10 @@ """The vizio component.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict, List +from typing import Any from pyvizio.const import APPS from pyvizio.util import gen_apps_list_from_url @@ -116,7 +118,7 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): ) self.data = APPS - async def _async_update_data(self) -> List[Dict[str, Any]]: + async def _async_update_data(self) -> list[dict[str, Any]]: """Update data via library.""" data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass)) if not data: diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 3f57cdb81fa..2c3c365b15a 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Vizio.""" +from __future__ import annotations + import copy import logging import socket -from typing import Any, Dict, Optional +from typing import Any from pyvizio import VizioAsync, async_guess_device_type from pyvizio.const import APP_HOME @@ -48,7 +50,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: +def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. @@ -76,7 +78,7 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: ) -def _get_pairing_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: +def _get_pairing_schema(input_dict: dict[str, Any] = None) -> vol.Schema: """ Return schema defaults for pairing data based on user input. @@ -108,8 +110,8 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): self.config_entry = config_entry async def async_step_init( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """Manage the vizio options.""" if user_input is not None: if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): @@ -191,7 +193,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data = None self._apps = {} - async def _create_entry(self, input_dict: Dict[str, Any]) -> Dict[str, Any]: + async def _create_entry(self, input_dict: dict[str, Any]) -> dict[str, Any]: """Create vizio config entry.""" # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) @@ -203,8 +205,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict) async def async_step_user( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """Handle a flow initialized by the user.""" errors = {} @@ -271,12 +273,13 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if errors and self.context["source"] == SOURCE_IMPORT: # Log an error message if import config flow fails since otherwise failure is silent _LOGGER.error( - "configuration.yaml import failure: %s", ", ".join(errors.values()) + "Importing from configuration.yaml failed: %s", + ", ".join(errors.values()), ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_import(self, import_config: Dict[str, Any]) -> Dict[str, Any]: + async def async_step_import(self, import_config: dict[str, Any]) -> dict[str, Any]: """Import a config entry from configuration.yaml.""" # Check if new config entry matches any existing config entries for entry in self.hass.config_entries.async_entries(DOMAIN): @@ -339,8 +342,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input=import_config) async def async_step_zeroconf( - self, discovery_info: Optional[DiscoveryInfoType] = None - ) -> Dict[str, Any]: + self, discovery_info: DiscoveryInfoType | None = None + ) -> dict[str, Any]: """Handle zeroconf discovery.""" # If host already has port, no need to add it again if ":" not in discovery_info[CONF_HOST]: @@ -376,8 +379,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input=discovery_info) async def async_step_pair_tv( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """ Start pairing process for TV. @@ -442,7 +445,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _pairing_complete(self, step_id: str) -> Dict[str, Any]: + async def _pairing_complete(self, step_id: str) -> dict[str, Any]: """Handle config flow completion.""" if not self._must_show_form: return await self._create_entry(self._data) @@ -455,8 +458,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_pairing_complete( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """ Complete non-import sourced config flow. @@ -465,8 +468,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._pairing_complete("pairing_complete") async def async_step_pairing_complete_import( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """ Complete import sourced config flow. diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 53c8a2bba88..fc955d48158 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,7 +1,9 @@ """Vizio SmartCast Device support.""" +from __future__ import annotations + from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name @@ -64,7 +66,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up a Vizio media player entry.""" host = config_entry.data[CONF_HOST] @@ -166,7 +168,7 @@ class VizioDevice(MediaPlayerEntity): self._model = None self._sw_version = None - def _apps_list(self, apps: List[str]) -> List[str]: + def _apps_list(self, apps: list[str]) -> list[str]: """Return process apps list based on configured filters.""" if self._conf_apps.get(CONF_INCLUDE): return [app for app in apps if app in self._conf_apps[CONF_INCLUDE]] @@ -274,7 +276,7 @@ class VizioDevice(MediaPlayerEntity): if self._current_app == NO_APP_RUNNING: self._current_app = None - def _get_additional_app_names(self) -> List[Dict[str, Any]]: + def _get_additional_app_names(self) -> list[dict[str, Any]]: """Return list of additional apps that were included in configuration.yaml.""" return [ additional_app["name"] for additional_app in self._additional_app_configs @@ -296,7 +298,7 @@ class VizioDevice(MediaPlayerEntity): self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) async def async_update_setting( - self, setting_type: str, setting_name: str, new_value: Union[int, str] + self, setting_type: str, setting_name: str, new_value: int | str ) -> None: """Update a setting when update_setting service is called.""" await self._device.set_setting( @@ -340,7 +342,7 @@ class VizioDevice(MediaPlayerEntity): return self._available @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return the state of the device.""" return self._state @@ -355,7 +357,7 @@ class VizioDevice(MediaPlayerEntity): return self._icon @property - def volume_level(self) -> Optional[float]: + def volume_level(self) -> float | None: """Return the volume level of the device.""" return self._volume_level @@ -365,7 +367,7 @@ class VizioDevice(MediaPlayerEntity): return self._is_volume_muted @property - def source(self) -> Optional[str]: + def source(self) -> str | None: """Return current input of the device.""" if self._current_app is not None and self._current_input in INPUT_APPS: return self._current_app @@ -373,7 +375,7 @@ class VizioDevice(MediaPlayerEntity): return self._current_input @property - def source_list(self) -> List[str]: + def source_list(self) -> list[str]: """Return list of available inputs of the device.""" # If Smartcast app is in input list, and the app list has been retrieved, # show the combination with , otherwise just return inputs @@ -395,7 +397,7 @@ class VizioDevice(MediaPlayerEntity): return self._available_inputs @property - def app_id(self) -> Optional[str]: + def app_id(self) -> str | None: """Return the ID of the current app if it is unknown by pyvizio.""" if self._current_app_config and self.app_name == UNKNOWN_APP: return { @@ -407,7 +409,7 @@ class VizioDevice(MediaPlayerEntity): return None @property - def app_name(self) -> Optional[str]: + def app_name(self) -> str | None: """Return the friendly name of the current app.""" return self._current_app @@ -422,7 +424,7 @@ class VizioDevice(MediaPlayerEntity): return self._config_entry.unique_id @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.unique_id)}, @@ -438,12 +440,12 @@ class VizioDevice(MediaPlayerEntity): return self._device_class @property - def sound_mode(self) -> Optional[str]: + def sound_mode(self) -> str | None: """Name of the current sound mode.""" return self._current_sound_mode @property - def sound_mode_list(self) -> Optional[List[str]]: + def sound_mode_list(self) -> list[str] | None: """List of available sound modes.""" return self._available_sound_modes diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 7401b751d27..6f0962509f5 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -2,9 +2,21 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { + "pair_tv": { + "data": { + "pin": "PIN-k\u00f3d" + } + }, + "pairing_complete": { + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz." + }, "user": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", @@ -12,7 +24,7 @@ "host": "Hoszt", "name": "N\u00e9v" }, - "title": "A Vizio SmartCast Client be\u00e1ll\u00edt\u00e1sa" + "title": "VIZIO SmartCast Eszk\u00f6z" } } }, @@ -22,7 +34,7 @@ "data": { "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga" }, - "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat" + "title": "VIZIO SmartCast Eszk\u00f6z be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } } diff --git a/homeassistant/components/vizio/translations/id.json b/homeassistant/components/vizio/translations/id.json new file mode 100644 index 00000000000..19f9b41449c --- /dev/null +++ b/homeassistant/components/vizio/translations/id.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "updated_entry": "Entri ini telah disiapkan tetapi nama, aplikasi, dan/atau opsi yang ditentukan dalam konfigurasi tidak cocok dengan konfigurasi yang diimpor sebelumnya, oleh karena itu entri konfigurasi telah diperbarui." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "complete_pairing_failed": "Tidak dapat menyelesaikan pemasangan. Pastikan PIN yang diberikan benar dan TV masih menyala dan terhubung ke jaringan sebelum mengirim ulang.", + "existing_config_entry_found": "Entri konfigurasi Perangkat VIZIO SmartCast yang ada dengan nomor seri yang sama telah dikonfigurasi. Anda harus menghapus entri yang ada untuk mengonfigurasi entri ini." + }, + "step": { + "pair_tv": { + "data": { + "pin": "Kode PIN" + }, + "description": "TV Anda harus menampilkan kode. Masukkan kode tersebut ke dalam formulir dan kemudian lanjutkan ke langkah berikutnya untuk menyelesaikan pemasangan.", + "title": "Selesaikan Proses Pemasangan" + }, + "pairing_complete": { + "description": "Perangkat VIZIO SmartCast sekarang terhubung ke Home Assistant.", + "title": "Pemasangan Selesai" + }, + "pairing_complete_import": { + "description": "Perangkat VIZIO SmartCast sekarang terhubung ke Home Assistant. \n\nToken Akses adalah '**{access_token}**'.", + "title": "Pemasangan Selesai" + }, + "user": { + "data": { + "access_token": "Token Akses", + "device_class": "Jenis Perangkat", + "host": "Host", + "name": "Nama" + }, + "description": "Token Akses hanya diperlukan untuk TV. Jika Anda mengkonfigurasi TV dan belum memiliki Token Akses , biarkan kosong untuk melakukan proses pemasangan.", + "title": "Perangkat VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "Aplikasi untuk Disertakan atau Dikecualikan", + "include_or_exclude": "Sertakan atau Kecualikan Aplikasi?", + "volume_step": "Langkah Volume" + }, + "description": "Jika memiliki Smart TV, Anda dapat memfilter daftar sumber secara opsional dengan memilih aplikasi mana yang akan disertakan atau dikecualikan dalam daftar sumber Anda.", + "title": "Perbarui Opsi Perangkat VIZIO SmartCast" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json index 037c85d7c4e..cc86e020a1c 100644 --- a/homeassistant/components/vizio/translations/ko.json +++ b/homeassistant/components/vizio/translations/ko.json @@ -7,23 +7,23 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0, TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "existing_config_entry_found": "\uc77c\ub828 \ubc88\ud638\uac00 \ub3d9\uc77c\ud55c \uae30\uc874 VIZIO SmartCast \uae30\uae30 \uad6c\uc131 \ud56d\ubaa9\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \ud56d\ubaa9\uc744 \uad6c\uc131\ud558\ub824\uba74 \uae30\uc874 \ud56d\ubaa9\uc744 \uc0ad\uc81c\ud574\uc57c\ud569\ub2c8\ub2e4." + "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN\uc774 \uc62c\ubc14\ub978\uc9c0, TV\uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "existing_config_entry_found": "\uc2dc\ub9ac\uc5bc \ubc88\ud638\uac00 \ub3d9\uc77c\ud55c VIZIO SmartCast \uae30\uae30 \uad6c\uc131 \ud56d\ubaa9\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc774 \ud56d\ubaa9\uc744 \uad6c\uc131\ud558\ub824\uba74 \uae30\uc874 \ud56d\ubaa9\uc744 \uc0ad\uc81c\ud574\uc57c \ud569\ub2c8\ub2e4." }, "step": { "pair_tv": { "data": { "pin": "PIN \ucf54\ub4dc" }, - "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc785\ub825\ub780\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", + "description": "TV\uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc785\ub825\ub780\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", "title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \ub05d\ub0b4\uae30" }, "pairing_complete": { - "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" }, "pairing_complete_import": { - "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.", + "description": "VIZIO SmartCast \uae30\uae30\uac00 Home Assistant\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.", "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" }, "user": { @@ -33,7 +33,7 @@ "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" }, - "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV \uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV \ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.", + "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV\uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV\ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.", "title": "VIZIO SmartCast \uae30\uae30" } } @@ -46,7 +46,7 @@ "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30" }, - "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\uc2a4\ub9c8\ud2b8 TV\uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "VIZIO SmartCast \uae30\uae30 \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" } } diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index 48fd831d61c..48a7a7d353d 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -7,12 +7,13 @@ }, "error": { "cannot_connect": "Verbinding mislukt", - "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt." + "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt.", + "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande invoer verwijderen om deze te kunnen configureren." }, "step": { "pair_tv": { "data": { - "pin": "PIN" + "pin": "PIN-code" }, "description": "Uw TV zou een code moeten weergeven. Voer die code in het formulier in en ga dan door naar de volgende stap om de koppeling te voltooien.", "title": "Voltooi het koppelingsproces" @@ -29,11 +30,11 @@ "data": { "access_token": "Toegangstoken", "device_class": "Apparaattype", - "host": ":", + "host": "Host", "name": "Naam" }, "description": "Een toegangstoken is alleen nodig voor tv's. Als u een TV configureert en nog geen toegangstoken heeft, laat dit dan leeg en doorloop het koppelingsproces.", - "title": "Vizio SmartCast Client instellen" + "title": "VIZIO SmartCast-apparaat" } } }, @@ -46,7 +47,7 @@ "volume_step": "Volume Stapgrootte" }, "description": "Als je een Smart TV hebt, kun je optioneel je bronnenlijst filteren door te kiezen welke apps je in je bronnenlijst wilt opnemen of uitsluiten.", - "title": "Update Vizo SmartCast Opties" + "title": "Update VIZIO SmartCast-apparaat opties" } } } diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index 5f21dd0c2b6..f4ac22716d1 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -46,7 +46,7 @@ "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f", "volume_step": "\u97f3\u91cf\u5927\u5c0f" }, - "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", + "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u7be9\u9078\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", "title": "\u66f4\u65b0 VIZIO SmartCast \u88dd\u7f6e \u9078\u9805" } } diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index fe387dd4adc..db5c26f4a0c 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -159,7 +159,7 @@ class VlcDevice(MediaPlayerEntity): def play_media(self, media_type, media_id, **kwargs): """Play media from a URL or file.""" - if not media_type == MEDIA_TYPE_MUSIC: + if media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 68b3c373c7a..784df2cabcf 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -36,6 +36,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -46,17 +47,17 @@ DEFAULT_PORT = 4212 MAX_VOLUME = 500 SUPPORT_VLC = ( - SUPPORT_PAUSE - | SUPPORT_SEEK - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - | SUPPORT_PREVIOUS_TRACK + SUPPORT_CLEAR_PLAYLIST | SUPPORT_NEXT_TRACK - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_PAUSE | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SEEK | SUPPORT_SHUFFLE_SET + | SUPPORT_STOP + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -125,7 +126,7 @@ class VlcDevice(MediaPlayerEntity): if status: if "volume" in status: - self._volume = int(status["volume"]) / 500.0 + self._volume = status["volume"] / MAX_VOLUME else: self._volume = None if "state" in status: @@ -141,7 +142,12 @@ class VlcDevice(MediaPlayerEntity): if self._state != STATE_IDLE: self._media_duration = self._vlc.get_length() - self._media_position = self._vlc.get_time() + vlc_position = self._vlc.get_time() + + # Check if current position is stale. + if vlc_position != self._media_position: + self._media_position_updated_at = dt_util.utcnow() + self._media_position = vlc_position info = self._vlc.info() _LOGGER.debug("Info: %s", info) @@ -150,6 +156,12 @@ class VlcDevice(MediaPlayerEntity): self._media_artist = info.get(0, {}).get("artist") self._media_title = info.get(0, {}).get("title") + if not self._media_title: + # Fall back to filename. + data_info = info.get("data") + if data_info: + self._media_title = data_info["filename"] + except (CommandError, LuaError, ParseError) as err: _LOGGER.error("Command error: %s", err) except (ConnErr, EOFError) as err: @@ -220,8 +232,7 @@ class VlcDevice(MediaPlayerEntity): def media_seek(self, position): """Seek the media to a specific location.""" - track_length = self._vlc.get_length() / 1000 - self._vlc.seek(position / track_length) + self._vlc.seek(int(position)) def mute_volume(self, mute): """Mute the volume.""" @@ -238,6 +249,10 @@ class VlcDevice(MediaPlayerEntity): self._vlc.set_volume(volume * MAX_VOLUME) self._volume = volume + if self._muted and self._volume > 0: + # This can happen if we were muted and then see a volume_up. + self._muted = False + def media_play(self): """Send play command.""" self._vlc.play() diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index a418780c165..61fcf1d2969 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -6,7 +6,7 @@ from volkszaehler import Volkszaehler from volkszaehler.exceptions import VolkszaehlerApiConnectionError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -18,7 +18,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class VolkszaehlerSensor(Entity): +class VolkszaehlerSensor(SensorEntity): """Implementation of a Volkszaehler sensor.""" def __init__(self, vz_api, name, sensor_type): diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index 8d171cab9d2..a9c6fb746aa 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -14,11 +14,6 @@ from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN PLATFORMS = ["media_player"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Volumio component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Volumio from a config entry.""" @@ -35,9 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): DATA_INFO: info, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -48,8 +43,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index 950a161a5c3..80ec2f05d91 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Volumio integration.""" +from __future__ import annotations + import logging -from typing import Optional from pyvolumio import CannotConnectError, Volumio import voluptuous as vol @@ -11,7 +12,7 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,10 +40,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" - self._host: Optional[str] = None - self._port: Optional[int] = None - self._name: Optional[str] = None - self._uuid: Optional[str] = None + self._host: str | None = None + self._port: int | None = None + self._name: str | None = None + self._uuid: str | None = None @callback def _async_get_entry(self): diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json index 3b2d79a34a7..e58f0666039 100644 --- a/homeassistant/components/volumio/translations/hu.json +++ b/homeassistant/components/volumio/translations/hu.json @@ -1,7 +1,24 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Nem lehet csatlakozni a felfedezett Volumi\u00f3hoz" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9d hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistant-hoz?", + "title": "Felfedezett Volumio" + }, + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/id.json b/homeassistant/components/volumio/translations/id.json new file mode 100644 index 00000000000..210c6eca87d --- /dev/null +++ b/homeassistant/components/volumio/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Tidak dapat terhubung ke Volumio yang ditemukan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "discovery_confirm": { + "description": "Ingin menambahkan Volumio (`{name}`) ke Home Assistant?", + "title": "Volumio yang ditemukan" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/ko.json b/homeassistant/components/volumio/translations/ko.json index 2c630e533ff..75ff87b41c8 100644 --- a/homeassistant/components/volumio/translations/ko.json +++ b/homeassistant/components/volumio/translations/ko.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\ubc1c\uacac\ub41c Volumio\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\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": { + "discovery_confirm": { + "description": "Home Assistant\uc5d0 Volumio (`{name}`)\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c Volumio" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/volumio/translations/nl.json b/homeassistant/components/volumio/translations/nl.json index 9e11dbad82b..96538422fe0 100644 --- a/homeassistant/components/volumio/translations/nl.json +++ b/homeassistant/components/volumio/translations/nl.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken met Volumio" }, "error": { "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { + "discovery_confirm": { + "description": "Wilt u Volumio (`{name}`) toevoegen aan Home Assistant?", + "title": "Volumio ontdekt" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 792fcc25eff..556a5f25114 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -39,7 +39,7 @@ CONF_MUTABLE = "mutable" SIGNAL_STATE_UPDATED = f"{DOMAIN}.updated" -COMPONENTS = { +PLATFORMS = { "sensor": "sensor", "binary_sensor": "binary_sensor", "lock": "lock", @@ -146,7 +146,7 @@ async def async_setup(hass, config): for instrument in ( instrument for instrument in dashboard.instruments - if instrument.component in COMPONENTS and is_enabled(instrument.slug_attr) + if instrument.component in PLATFORMS and is_enabled(instrument.slug_attr) ): data.instruments.add(instrument) @@ -154,7 +154,7 @@ async def async_setup(hass, config): hass.async_create_task( discovery.async_load_platform( hass, - COMPONENTS[instrument.component], + PLATFORMS[instrument.component], DOMAIN, (vehicle.vin, instrument.component, instrument.attr), config, @@ -277,7 +277,7 @@ class VolvoEntity(Entity): return True @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return dict( self.instrument.attributes, diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index 0915408860d..ad6571576b4 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -1,4 +1,6 @@ """Support for Volvo On Call sensors.""" +from homeassistant.components.sensor import SensorEntity + from . import DATA_KEY, VolvoEntity @@ -9,7 +11,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)]) -class VolvoSensor(VolvoEntity): +class VolvoSensor(VolvoEntity, SensorEntity): """Representation of a Volvo sensor.""" @property diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index c1b60479e7a..c62d5136aa6 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -86,7 +86,7 @@ class VultrBinarySensor(BinarySensorEntity): return DEFAULT_DEVICE_CLASS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Vultr subscription.""" return { ATTR_ALLOWED_BANDWIDTH: self.data.get("allowed_bandwidth_gb"), diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index 0bcdcf9d4c1..5e6815944d7 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -3,10 +3,9 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, DATA_GIGABYTES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import ( ATTR_CURRENT_BANDWIDTH_USED, @@ -58,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class VultrSensor(Entity): +class VultrSensor(SensorEntity): """Representation of a Vultr subscription sensor.""" def __init__(self, vultr, subscription, condition, name): diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py index a9c43717a71..f93c4f444d6 100644 --- a/homeassistant/components/vultr/switch.py +++ b/homeassistant/components/vultr/switch.py @@ -80,7 +80,7 @@ class VultrSwitch(SwitchEntity): return "mdi:server" if self.is_on else "mdi:server-off" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the Vultr subscription.""" return { ATTR_ALLOWED_BANDWIDTH: self.data.get("allowed_bandwidth_gb"), diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index c66f87ae26e..bcd7ef58c8c 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -3,5 +3,5 @@ "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "requirements": ["wakeonlan==1.1.6"], - "codeowners": [] + "codeowners": ["@ntilley905"] } diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 491bae782c5..eba6897647b 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -57,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): broadcast_port, ) ], - True, + host is not None, ) @@ -86,6 +86,7 @@ class WolSwitch(SwitchEntity): Script(hass, off_action, name, domain) if off_action else None ) self._state = False + self._assumed_state = host is None @property def is_on(self): @@ -97,6 +98,16 @@ class WolSwitch(SwitchEntity): """Return the name of the switch.""" return self._name + @property + def assumed_state(self): + """Return true if no host is provided.""" + return self._assumed_state + + @property + def should_poll(self): + """Return false if assumed state is true.""" + return not self._assumed_state + def turn_on(self, **kwargs): """Turn the device on.""" service_kwargs = {} @@ -114,13 +125,21 @@ class WolSwitch(SwitchEntity): wakeonlan.send_magic_packet(self._mac_address, **service_kwargs) + if self._assumed_state: + self._state = True + self.async_write_ha_state() + def turn_off(self, **kwargs): """Turn the device off if an off action is present.""" if self._off_script is not None: self._off_script.run(context=self._context) + if self._assumed_state: + self._state = False + self.async_write_ha_state() + def update(self): - """Check if device is on and update the state.""" + """Check if device is on and update the state. Only called if assumed state is false.""" if platform.system().lower() == "windows": ping_cmd = [ "ping", diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index ec18880b5ba..ef01c057a9e 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -7,6 +7,7 @@ import aiohttp import voluptuous as vol from waqiasync import WaqiClient +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_TEMPERATURE, @@ -17,7 +18,6 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -93,7 +93,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class WaqiSensor(Entity): +class WaqiSensor(SensorEntity): """Implementation of a WAQI sensor.""" def __init__(self, client, station): @@ -151,7 +151,7 @@ class WaqiSensor(Entity): return "AQI" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the last update.""" attrs = {} diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 0763c552075..5ae22c77b5e 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import final import voluptuous as vol @@ -129,7 +130,7 @@ async def async_unload_entry(hass, entry): class WaterHeaterEntity(Entity): - """Representation of a water_heater device.""" + """Base class for water heater entities.""" @property def state(self): @@ -162,6 +163,7 @@ class WaterHeaterEntity(Entity): return data + @final @property def state_attributes(self): """Return the optional state attributes.""" diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py index 991929580d1..e1c84be8753 100644 --- a/homeassistant/components/water_heater/device_action.py +++ b/homeassistant/components/water_heater/device_action.py @@ -1,5 +1,5 @@ """Provides device automations for Water Heater.""" -from typing import List, Optional +from __future__ import annotations import voluptuous as vol @@ -28,7 +28,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions for Water Heater devices.""" registry = await entity_registry.async_get_registry(hass) actions = [] @@ -58,11 +58,9 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] + hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - config = ACTION_SCHEMA(config) - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} if config[CONF_TYPE] == "turn_on": diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py index 77cdba93f96..4675cdb8621 100644 --- a/homeassistant/components/water_heater/reproduce_state.py +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -1,7 +1,9 @@ """Reproduce an Water heater state.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Iterable from homeassistant.const import ( ATTR_ENTITY_ID, @@ -47,8 +49,8 @@ async def _async_reproduce_state( hass: HomeAssistantType, state: State, *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" cur_state = hass.states.get(state.entity_id) @@ -124,8 +126,8 @@ async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce Water heater states.""" await asyncio.gather( diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 8f5709ac155..71fc1dc9328 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -4,5 +4,16 @@ "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" } + }, + "state": { + "_": { + "off": "[%key:common::state::off%]", + "eco": "Eco", + "electric": "Electric", + "gas": "Gas", + "high_demand": "High Demand", + "heat_pump": "Heat Pump", + "performance": "Performance" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/ca.json b/homeassistant/components/water_heater/translations/ca.json index 8c1de7124f5..022b5e887d8 100644 --- a/homeassistant/components/water_heater/translations/ca.json +++ b/homeassistant/components/water_heater/translations/ca.json @@ -4,5 +4,16 @@ "turn_off": "Apaga {entity_name}", "turn_on": "Engega {entity_name}" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "El\u00e8ctric", + "gas": "Gas", + "heat_pump": "Bomba de calor", + "high_demand": "Alta demanda", + "off": "OFF", + "performance": "Rendiment" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/cs.json b/homeassistant/components/water_heater/translations/cs.json index c7152528be9..9aa18313883 100644 --- a/homeassistant/components/water_heater/translations/cs.json +++ b/homeassistant/components/water_heater/translations/cs.json @@ -4,5 +4,10 @@ "turn_off": "Vypnout {entity_name}", "turn_on": "Zapnout {entity_name}" } + }, + "state": { + "_": { + "off": "Vypnuto" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/de.json b/homeassistant/components/water_heater/translations/de.json index 9169018a5d4..86f61158351 100644 --- a/homeassistant/components/water_heater/translations/de.json +++ b/homeassistant/components/water_heater/translations/de.json @@ -4,5 +4,16 @@ "turn_off": "{entity_name} ausschalten", "turn_on": "{entity_name} einschalten" } + }, + "state": { + "_": { + "eco": "Sparmodus", + "electric": "Elektrisch", + "gas": "Gas", + "heat_pump": "W\u00e4rmepumpe", + "high_demand": "Hoher Bedarf", + "off": "Aus", + "performance": "Leistung" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/el.json b/homeassistant/components/water_heater/translations/el.json new file mode 100644 index 00000000000..827a171a7ac --- /dev/null +++ b/homeassistant/components/water_heater/translations/el.json @@ -0,0 +1,13 @@ +{ + "state": { + "_": { + "eco": "\u039f\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc", + "electric": "\u0397\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03cc", + "gas": "\u0391\u03ad\u03c1\u03b9\u03bf", + "heat_pump": "\u0391\u03bd\u03c4\u03bb\u03af\u03b1 \u0398\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "high_demand": "\u03a5\u03c8\u03b7\u03bb\u03ae \u0396\u03ae\u03c4\u03b7\u03c3\u03b7", + "off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "performance": "\u0391\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/en.json b/homeassistant/components/water_heater/translations/en.json index d6abfbb995d..6885fcb198e 100644 --- a/homeassistant/components/water_heater/translations/en.json +++ b/homeassistant/components/water_heater/translations/en.json @@ -4,5 +4,16 @@ "turn_off": "Turn off {entity_name}", "turn_on": "Turn on {entity_name}" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "Electric", + "gas": "Gas", + "heat_pump": "Heat Pump", + "high_demand": "High Demand", + "off": "Off", + "performance": "Performance" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/es.json b/homeassistant/components/water_heater/translations/es.json index 46be0201ba5..f11f9592b81 100644 --- a/homeassistant/components/water_heater/translations/es.json +++ b/homeassistant/components/water_heater/translations/es.json @@ -4,5 +4,15 @@ "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "El\u00e9ctrico", + "gas": "Gas", + "heat_pump": "Bomba de calor", + "high_demand": "Alta demanda", + "performance": "Rendimiento" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/et.json b/homeassistant/components/water_heater/translations/et.json index fbb3d11424f..cba6b8274d9 100644 --- a/homeassistant/components/water_heater/translations/et.json +++ b/homeassistant/components/water_heater/translations/et.json @@ -4,5 +4,16 @@ "turn_off": "L\u00fclita {entity_name} v\u00e4lja", "turn_on": "L\u00fclita {entity_name} sisse" } + }, + "state": { + "_": { + "eco": "\u00d6ko", + "electric": "Elektriline", + "gas": "Gaas", + "heat_pump": "Soojuspump", + "high_demand": "Suur n\u00f5udlus", + "off": "V\u00e4lja l\u00fclitatud", + "performance": "J\u00f5udlus" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/fr.json b/homeassistant/components/water_heater/translations/fr.json index ac72ffab883..84c9b730b22 100644 --- a/homeassistant/components/water_heater/translations/fr.json +++ b/homeassistant/components/water_heater/translations/fr.json @@ -4,5 +4,16 @@ "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" } + }, + "state": { + "_": { + "eco": "\u00c9co", + "electric": "\u00c9lectrique", + "gas": "Gaz", + "heat_pump": "Pompe \u00e0 chaleur", + "high_demand": "Demande \u00e9lev\u00e9e", + "off": "Inactif", + "performance": "Performance" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/hu.json b/homeassistant/components/water_heater/translations/hu.json index c3c47030acb..82f88d1f0de 100644 --- a/homeassistant/components/water_heater/translations/hu.json +++ b/homeassistant/components/water_heater/translations/hu.json @@ -4,5 +4,16 @@ "turn_off": "{entity_name} kikapcsol\u00e1sa", "turn_on": "{entity_name} bekapcsol\u00e1sa" } + }, + "state": { + "_": { + "eco": "Takar\u00e9kos", + "electric": "Elektromos", + "gas": "G\u00e1z", + "heat_pump": "H\u0151szivatty\u00fa", + "high_demand": "Magas ig\u00e9nybev\u00e9tel", + "off": "Ki", + "performance": "Teljes\u00edtm\u00e9ny" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/id.json b/homeassistant/components/water_heater/translations/id.json new file mode 100644 index 00000000000..591f96ffc4f --- /dev/null +++ b/homeassistant/components/water_heater/translations/id.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Matikan {entity_name}", + "turn_on": "Nyalakan {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/it.json b/homeassistant/components/water_heater/translations/it.json index 86458a54272..88c53f67e3a 100644 --- a/homeassistant/components/water_heater/translations/it.json +++ b/homeassistant/components/water_heater/translations/it.json @@ -4,5 +4,16 @@ "turn_off": "Disattiva {entity_name}", "turn_on": "Attiva {entity_name}" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "Elettrico", + "gas": "Gas", + "heat_pump": "Pompa di calore", + "high_demand": "Forte richiesta", + "off": "Spento", + "performance": "Prestazione" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/ko.json b/homeassistant/components/water_heater/translations/ko.json new file mode 100644 index 00000000000..8c591c19918 --- /dev/null +++ b/homeassistant/components/water_heater/translations/ko.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", + "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" + } + }, + "state": { + "_": { + "eco": "\uc808\uc57d", + "electric": "\uc804\uae30", + "gas": "\uac00\uc2a4", + "heat_pump": "\ud788\ud2b8 \ud38c\ud504", + "high_demand": "\uace0\uc131\ub2a5", + "off": "\uaebc\uc9d0", + "performance": "\uace0\ud6a8\uc728" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/nl.json b/homeassistant/components/water_heater/translations/nl.json index 8b832b52c1c..4a2d1357188 100644 --- a/homeassistant/components/water_heater/translations/nl.json +++ b/homeassistant/components/water_heater/translations/nl.json @@ -4,5 +4,16 @@ "turn_off": "Schakel {entity_name} uit", "turn_on": "Schakel {entity_name} in" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "Elektriciteit", + "gas": "Gas", + "heat_pump": "Warmtepomp", + "high_demand": "Hoge vraag", + "off": "Uit", + "performance": "Prestaties" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/no.json b/homeassistant/components/water_heater/translations/no.json index 6455919518f..2cbb96b23e7 100644 --- a/homeassistant/components/water_heater/translations/no.json +++ b/homeassistant/components/water_heater/translations/no.json @@ -4,5 +4,16 @@ "turn_off": "Sl\u00e5 av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" } + }, + "state": { + "_": { + "eco": "\u00d8ko", + "electric": "Elektrisk", + "gas": "Gass", + "heat_pump": "Varmepumpe", + "high_demand": "H\u00f8y ettersp\u00f8rsel", + "off": "Av", + "performance": "Ytelse" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/pl.json b/homeassistant/components/water_heater/translations/pl.json index d6deb99da02..bb314c85510 100644 --- a/homeassistant/components/water_heater/translations/pl.json +++ b/homeassistant/components/water_heater/translations/pl.json @@ -4,5 +4,16 @@ "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" } + }, + "state": { + "_": { + "eco": "ekonomiczny", + "electric": "elektryczny", + "gas": "gaz", + "heat_pump": "pompa ciep\u0142a", + "high_demand": "du\u017ce zapotrzebowanie", + "off": "wy\u0142.", + "performance": "wydajno\u015b\u0107" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/pt.json b/homeassistant/components/water_heater/translations/pt.json index 2278e7701aa..97cc30fe135 100644 --- a/homeassistant/components/water_heater/translations/pt.json +++ b/homeassistant/components/water_heater/translations/pt.json @@ -4,5 +4,15 @@ "turn_off": "Desligar {entity_name}", "turn_on": "Ligar {entity_name}" } + }, + "state": { + "_": { + "eco": "Eco", + "electric": "El\u00e9trico", + "gas": "G\u00e1s", + "heat_pump": "Bomba de calor", + "high_demand": "Necessidade alta", + "off": "Desligado" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/ru.json b/homeassistant/components/water_heater/translations/ru.json index 7c703da39d5..b3491f82e5e 100644 --- a/homeassistant/components/water_heater/translations/ru.json +++ b/homeassistant/components/water_heater/translations/ru.json @@ -4,5 +4,16 @@ "turn_off": "{entity_name}: \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c", "turn_on": "{entity_name}: \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c" } + }, + "state": { + "_": { + "eco": "\u042d\u043a\u043e", + "electric": "\u042d\u043b\u0435\u043a\u0442\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0439", + "gas": "\u0413\u0430\u0437", + "heat_pump": "\u0422\u0435\u043f\u043b\u043e\u0432\u043e\u0439 \u043d\u0430\u0441\u043e\u0441", + "high_demand": "\u0411\u043e\u043b\u044c\u0448\u0430\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430", + "off": "\u0412\u044b\u043a\u043b", + "performance": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" + } } } \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/zh-Hant.json b/homeassistant/components/water_heater/translations/zh-Hant.json index 11e88cb1fae..5bb7aa129b5 100644 --- a/homeassistant/components/water_heater/translations/zh-Hant.json +++ b/homeassistant/components/water_heater/translations/zh-Hant.json @@ -4,5 +4,16 @@ "turn_off": "\u95dc\u9589{entity_name}", "turn_on": "\u958b\u555f{entity_name}" } + }, + "state": { + "_": { + "eco": "\u7bc0\u80fd", + "electric": "\u96fb\u529b", + "gas": "\u74e6\u65af", + "heat_pump": "\u6696\u6c23", + "high_demand": "\u9ad8\u7528\u91cf", + "off": "\u95dc\u9589", + "performance": "\u6548\u80fd" + } } } \ No newline at end of file diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index dfb960fe819..91e455d03d6 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -1,9 +1,8 @@ """Support for Waterfurnace.""" -from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from . import DOMAIN as WF_DOMAIN, UPDATE_TOPIC @@ -61,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class WaterFurnaceSensor(Entity): +class WaterFurnaceSensor(SensorEntity): """Implementing the Waterfurnace sensor.""" def __init__(self, client, config): diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 0357825cb12..327a0769b50 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -6,7 +6,7 @@ import re import WazeRouteCalculator import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -20,7 +20,6 @@ from homeassistant.const import ( ) from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -126,7 +125,7 @@ def _get_location_from_attributes(state): return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) -class WazeTravelTime(Entity): +class WazeTravelTime(SensorEntity): """Representation of a Waze travel time sensor.""" def __init__(self, name, origin, destination, waze_data): @@ -176,7 +175,7 @@ class WazeTravelTime(Entity): return ICON @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the last update.""" if self._waze_data.duration is None: return None diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 5127dae1102..da66c354d5a 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,6 +1,7 @@ """Weather component that handles meteorological data for your location.""" from datetime import timedelta import logging +from typing import final from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -138,6 +139,7 @@ class WeatherEntity(Entity): else PRECISION_WHOLE ) + @final @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/weather/group.py b/homeassistant/components/weather/group.py index 4741f8a3b54..2ac081496cd 100644 --- a/homeassistant/components/weather/group.py +++ b/homeassistant/components/weather/group.py @@ -2,13 +2,12 @@ from homeassistant.components.group import GroupIntegrationRegistry -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.exclude_domain() diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 9c6dfe45e74..6d61f5d62dc 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -89,7 +89,7 @@ async def async_handle_webhook(hass, webhook_id, request): # Look at content to provide some context for received webhook # Limit to 64 chars to avoid flooding the log content = await request.content.read(64) - _LOGGER.debug("%s...", content) + _LOGGER.debug("%s", content) return Response(status=HTTP_OK) try: diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 81c4c2813cd..a82dd0251c9 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -17,7 +17,7 @@ TRIGGER_SCHEMA = vol.Schema( ) -async def _handle_webhook(job, hass, webhook_id, request): +async def _handle_webhook(job, trigger_id, hass, webhook_id, request): """Handle incoming webhook.""" result = {"platform": "webhook", "webhook_id": webhook_id} @@ -28,18 +28,20 @@ async def _handle_webhook(job, hass, webhook_id, request): result["query"] = request.query result["description"] = "webhook" + result["id"] = trigger_id hass.async_run_hass_job(job, {"trigger": result}) async def async_attach_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None webhook_id = config.get(CONF_WEBHOOK_ID) job = HassJob(action) hass.components.webhook.async_register( automation_info["domain"], automation_info["name"], webhook_id, - partial(_handle_webhook, job), + partial(_handle_webhook, job, trigger_id), ) @callback diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index ebac158c452..681c2acfe01 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,25 +1,17 @@ """Support for LG webOS Smart TV.""" import asyncio +from contextlib import suppress +import json import logging +import os from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient +from sqlitedict import SqliteDict import voluptuous as vol from websockets.exceptions import ConnectionClosed -from homeassistant.components.webostv.const import ( - ATTR_BUTTON, - ATTR_COMMAND, - ATTR_PAYLOAD, - CONF_ON_ACTION, - CONF_SOURCES, - DEFAULT_NAME, - DOMAIN, - SERVICE_BUTTON, - SERVICE_COMMAND, - SERVICE_SELECT_SOUND_OUTPUT, - WEBOSTV_CONFIG_FILE, -) from homeassistant.const import ( + ATTR_COMMAND, ATTR_ENTITY_ID, CONF_CUSTOMIZE, CONF_HOST, @@ -30,7 +22,19 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ATTR_SOUND_OUTPUT +from .const import ( + ATTR_BUTTON, + ATTR_PAYLOAD, + ATTR_SOUND_OUTPUT, + CONF_ON_ACTION, + CONF_SOURCES, + DEFAULT_NAME, + DOMAIN, + SERVICE_BUTTON, + SERVICE_COMMAND, + SERVICE_SELECT_SOUND_OUTPUT, + WEBOSTV_CONFIG_FILE, +) CUSTOMIZE_SCHEMA = vol.Schema( {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} @@ -101,13 +105,41 @@ async def async_setup(hass, config): return True +def convert_client_keys(config_file): + """In case the config file contains JSON, convert it to a Sqlite config file.""" + # Return early if config file is non-existing + if not os.path.isfile(config_file): + return + + # Try to parse the file as being JSON + with open(config_file) as json_file: + try: + json_conf = json.load(json_file) + except (json.JSONDecodeError, UnicodeDecodeError): + json_conf = None + + # If the file contains JSON, convert it to an Sqlite DB + if json_conf: + _LOGGER.warning("LG webOS TV client-key file is being migrated to Sqlite!") + + # Clean the JSON file + os.remove(config_file) + + # Write the data to the Sqlite DB + with SqliteDict(config_file) as conf: + for host, key in json_conf.items(): + conf[host] = key + conf.commit() + + async def async_setup_tv(hass, config, conf): """Set up a LG WebOS TV based on host parameter.""" host = conf[CONF_HOST] config_file = hass.config.path(WEBOSTV_CONFIG_FILE) + await hass.async_add_executor_job(convert_client_keys, config_file) - client = WebOsClient(host, config_file) + client = await WebOsClient.create(host, config_file) hass.data[DOMAIN][host] = {"client": client} if client.is_registered(): @@ -119,9 +151,7 @@ async def async_setup_tv(hass, config, conf): async def async_connect(client): """Attempt a connection, but fail gracefully if tv is off for example.""" - try: - await client.connect() - except ( + with suppress( OSError, ConnectionClosed, ConnectionRefusedError, @@ -130,7 +160,7 @@ async def async_connect(client): PyLGTVPairException, PyLGTVCmdException, ): - pass + await client.connect() async def async_setup_tv_finalize(hass, config, conf, client): diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index bea485a7d68..9091491a29d 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -4,7 +4,6 @@ DOMAIN = "webostv" DEFAULT_NAME = "LG webOS Smart TV" ATTR_BUTTON = "button" -ATTR_COMMAND = "command" ATTR_PAYLOAD = "payload" ATTR_SOUND_OUTPUT = "sound_output" diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index acdee1d9ca9..7773e9c4963 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -2,7 +2,7 @@ "domain": "webostv", "name": "LG webOS Smart TV", "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiopylgtv==0.3.3"], + "requirements": ["aiopylgtv==0.4.0"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 4807d780a48..d94ab8a7c26 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,5 +1,6 @@ """Support for interface with an LG webOS Smart TV.""" import asyncio +from contextlib import suppress from datetime import timedelta from functools import wraps import logging @@ -214,9 +215,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): async def async_update(self): """Connect.""" if not self._client.is_connected(): - try: - await self._client.connect() - except ( + with suppress( OSError, ConnectionClosed, ConnectionRefusedError, @@ -225,7 +224,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): PyLGTVPairException, PyLGTVCmdException, ): - pass + await self._client.connect() @property def unique_id(self): @@ -271,7 +270,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): @property def source_list(self): """List of available input sources.""" - return sorted(list(self._source_list)) + return sorted(self._source_list) @property def media_content_type(self): @@ -318,7 +317,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): return supported @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" if self._client.sound_output is None and self.state == STATE_OFF: return {} @@ -386,7 +385,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) if media_type == MEDIA_TYPE_CHANNEL: - _LOGGER.debug("Searching channel...") + _LOGGER.debug("Searching channel") partial_match_channel_id = None perfect_match_channel_id = None diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 2d591455eaf..e7b10e18889 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -1,14 +1,16 @@ """WebSocket based API for Home Assistant.""" -from typing import Optional, Union, cast +from __future__ import annotations + +from typing import cast import voluptuous as vol from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass -from . import commands, connection, const, decorators, http, messages # noqa -from .connection import ActiveConnection # noqa -from .const import ( # noqa +from . import commands, connection, const, decorators, http, messages # noqa: F401 +from .connection import ActiveConnection # noqa: F401 +from .const import ( # noqa: F401 ERR_HOME_ASSISTANT_ERROR, ERR_INVALID_FORMAT, ERR_NOT_FOUND, @@ -19,13 +21,13 @@ from .const import ( # noqa ERR_UNKNOWN_COMMAND, ERR_UNKNOWN_ERROR, ) -from .decorators import ( # noqa +from .decorators import ( # noqa: F401 async_response, require_admin, websocket_command, ws_require_user, ) -from .messages import ( # noqa +from .messages import ( # noqa: F401 BASE_COMMAND_MESSAGE_SCHEMA, error_message, event_message, @@ -43,9 +45,9 @@ DEPENDENCIES = ("http",) @callback def async_register_command( hass: HomeAssistant, - command_or_handler: Union[str, const.WebSocketCommandHandler], - handler: Optional[const.WebSocketCommandHandler] = None, - schema: Optional[vol.Schema] = None, + command_or_handler: str | const.WebSocketCommandHandler, + handler: const.WebSocketCommandHandler | None = None, + schema: vol.Schema | None = None, ) -> None: """Register a websocket command.""" # pylint: disable=protected-access diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 53531cf9ba9..74251e1bf24 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -17,6 +17,7 @@ from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.setup import async_get_loaded_integrations from . import const, decorators, messages @@ -26,19 +27,20 @@ from . import const, decorators, messages @callback def async_register_commands(hass, async_reg): """Register commands.""" - async_reg(hass, handle_subscribe_events) - async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_call_service) - async_reg(hass, handle_get_states) - async_reg(hass, handle_get_services) + async_reg(hass, handle_entity_source) + async_reg(hass, handle_execute_script) async_reg(hass, handle_get_config) + async_reg(hass, handle_get_services) + async_reg(hass, handle_get_states) + async_reg(hass, handle_manifest_get) + async_reg(hass, handle_manifest_list) async_reg(hass, handle_ping) async_reg(hass, handle_render_template) - async_reg(hass, handle_manifest_list) - async_reg(hass, handle_manifest_get) - async_reg(hass, handle_entity_source) + async_reg(hass, handle_subscribe_events) async_reg(hass, handle_subscribe_trigger) async_reg(hass, handle_test_condition) + async_reg(hass, handle_unsubscribe_events) def pong_message(iden): @@ -214,13 +216,9 @@ def handle_get_config(hass, connection, msg): @decorators.async_response async def handle_manifest_list(hass, connection, msg): """Handle integrations command.""" + loaded_integrations = async_get_loaded_integrations(hass) integrations = await asyncio.gather( - *[ - async_get_integration(hass, domain) - for domain in hass.config.components - # Filter out platforms. - if "." not in domain - ] + *[async_get_integration(hass, domain) for domain in loaded_integrations] ) connection.send_result( msg["id"], [integration.manifest for integration in integrations] @@ -420,3 +418,23 @@ async def handle_test_condition(hass, connection, msg): connection.send_result( msg["id"], {"result": check_condition(hass, msg.get("variables"))} ) + + +@decorators.websocket_command( + { + vol.Required("type"): "execute_script", + vol.Required("sequence"): cv.SCRIPT_SCHEMA, + } +) +@decorators.require_admin +@decorators.async_response +async def handle_execute_script(hass, connection, msg): + """Handle execute script command.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from homeassistant.helpers.script import Script + + context = connection.context(msg) + script_obj = Script(hass, msg["sequence"], f"{const.DOMAIN} script", const.DOMAIN) + await script_obj.async_run(context=context) + connection.send_message(messages.result_message(msg["id"], {"context": context})) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 108d4de5ada..dd1bb333693 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,6 +1,8 @@ """Connection session.""" +from __future__ import annotations + import asyncio -from typing import Any, Callable, Dict, Hashable, Optional +from typing import Any, Callable, Hashable import voluptuous as vol @@ -26,7 +28,7 @@ class ActiveConnection: else: self.refresh_token_id = None - self.subscriptions: Dict[Hashable, Callable[[], Any]] = {} + self.subscriptions: dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 def context(self, msg): @@ -37,7 +39,7 @@ class ActiveConnection: return Context(user_id=user.id) @callback - def send_result(self, msg_id: int, result: Optional[Any] = None) -> None: + def send_result(self, msg_id: int, result: Any | None = None) -> None: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 5f2cfb2257d..7c3f18f856c 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder if TYPE_CHECKING: - from .connection import ActiveConnection # noqa + from .connection import ActiveConnection WebSocketCommandHandler = Callable[[HomeAssistant, "ActiveConnection", dict], None] diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index daa8529e8bd..27af0424f3c 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -1,8 +1,9 @@ """View to accept incoming websocket connection.""" +from __future__ import annotations + import asyncio from contextlib import suppress import logging -from typing import Optional from aiohttp import WSMsgType, web import async_timeout @@ -57,7 +58,7 @@ class WebSocketHandler: """Initialize an active connection.""" self.hass = hass self.request = request - self.wsock: Optional[web.WebSocketResponse] = None + self.wsock: web.WebSocketResponse | None = None self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None @@ -73,11 +74,11 @@ class WebSocketHandler: if message is None: break - self._logger.debug("Sending %s", message) - if not isinstance(message, str): message = message_to_json(message) + self._logger.debug("Sending %s", message) + await self.wsock.send_str(message) # Clean up the peaker checker when we shut down the writer diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index f68beff5924..736a7ad59f0 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -1,8 +1,9 @@ """Message templates for websocket commands.""" +from __future__ import annotations from functools import lru_cache import logging -from typing import Any, Dict +from typing import Any import voluptuous as vol @@ -32,12 +33,12 @@ IDEN_TEMPLATE = "__IDEN__" IDEN_JSON_TEMPLATE = '"__IDEN__"' -def result_message(iden: int, result: Any = None) -> Dict: +def result_message(iden: int, result: Any = None) -> dict: """Return a success result message.""" return {"id": iden, "type": const.TYPE_RESULT, "success": True, "result": result} -def error_message(iden: int, code: str, message: str) -> Dict: +def error_message(iden: int, code: str, message: str) -> dict: """Return an error result message.""" return { "id": iden, @@ -47,7 +48,7 @@ def error_message(iden: int, code: str, message: str) -> Dict: } -def event_message(iden: JSON_TYPE, event: Any) -> Dict: +def event_message(iden: JSON_TYPE, event: Any) -> dict: """Return an event message.""" return {"id": iden, "type": "event", "event": event} diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index c026978634f..dfcdc57842e 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -1,7 +1,7 @@ """Entity to track connections to websocket API.""" +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from .const import ( DATA_CONNECTIONS, @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([entity]) -class APICount(Entity): +class APICount(SensorEntity): """Entity to represent how many people are connected to the stream API.""" def __init__(self): diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index db380ae11ca..a013d1fdd34 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -113,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): static_conf = config.get(CONF_STATIC, []) if static_conf: - _LOGGER.debug("Adding statically configured WeMo devices...") + _LOGGER.debug("Adding statically configured WeMo devices") for device in await asyncio.gather( *[ hass.async_add_executor_job(validate_static_config, host, port) @@ -190,7 +190,7 @@ class WemoDiscovery: async def async_discover_and_schedule(self, *_) -> None: """Periodically scan the network looking for WeMo devices.""" - _LOGGER.debug("Scanning network for WeMo devices...") + _LOGGER.debug("Scanning network for WeMo devices") try: for device in await self._hass.async_add_executor_job( pywemo.discover_devices diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 4fac786af9a..d10707f4590 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -1,8 +1,10 @@ """Classes shared among Wemo entities.""" +from __future__ import annotations + import asyncio import contextlib import logging -from typing import Any, Dict, Generator, Optional +from typing import Any, Generator import async_timeout from pywemo import WeMoDevice @@ -19,7 +21,7 @@ class ExceptionHandlerStatus: """Exit status from the _wemo_exception_handler context manager.""" # An exception if one was raised in the _wemo_exception_handler. - exception: Optional[Exception] = None + exception: Exception | None = None @property def success(self) -> bool: @@ -68,7 +70,7 @@ class WemoEntity(Entity): _LOGGER.info("Reconnected to %s", self.name) self._available = True - def _update(self, force_update: Optional[bool] = True): + def _update(self, force_update: bool | None = True): """Update the device state.""" raise NotImplementedError() @@ -99,7 +101,7 @@ class WemoEntity(Entity): self._available = False async def _async_locked_update( - self, force_update: bool, timeout: Optional[async_timeout.timeout] = None + self, force_update: bool, timeout: async_timeout.timeout | None = None ) -> None: """Try updating within an async lock.""" async with self._update_lock: @@ -124,7 +126,7 @@ class WemoSubscriptionEntity(WemoEntity): return self.wemo.serialnumber @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return the device info.""" return { "name": self.name, @@ -142,8 +144,15 @@ class WemoSubscriptionEntity(WemoEntity): def should_poll(self) -> bool: """Return True if the the device requires local polling, False otherwise. + It is desirable to allow devices to enter periods of polling when the + callback subscription (device push) is not working. To work with the + entity platform polling logic, this entity needs to report True for + should_poll initially. That is required to cause the entity platform + logic to start the polling task (see the discussion in #47182). + Polling can be disabled if three conditions are met: - 1. The device has polled to get the initial state (self._has_polled). + 1. The device has polled to get the initial state (self._has_polled) and + to satisfy the entity platform constraint mentioned above. 2. The polling was successful and the device is in a healthy state (self.available). 3. The pywemo subscription registry reports that there is an active diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 1f45194659d..a3da5edae76 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -115,7 +115,7 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): return "mdi:water-percent" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" return { ATTR_CURRENT_HUMIDITY: self._current_humidity, @@ -127,7 +127,7 @@ class WemoHumidifier(WemoSubscriptionEntity, FanEntity): } @property - def percentage(self) -> str: + def percentage(self) -> int: """Return the current speed percentage.""" return ranged_value_to_percentage(SPEED_RANGE, self._fan_mode) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 15b38550b93..5e97031786c 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -59,7 +59,7 @@ class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): self._mode_string = None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" attr = {} if self.maker_params: diff --git a/homeassistant/components/wemo/translations/hu.json b/homeassistant/components/wemo/translations/hu.json new file mode 100644 index 00000000000..ab799e90c74 --- /dev/null +++ b/homeassistant/components/wemo/translations/hu.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/id.json b/homeassistant/components/wemo/translations/id.json new file mode 100644 index 00000000000..af0b3128cb9 --- /dev/null +++ b/homeassistant/components/wemo/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan Wemo?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/ko.json b/homeassistant/components/wemo/translations/ko.json index 5673c049422..704b1126261 100644 --- a/homeassistant/components/wemo/translations/ko.json +++ b/homeassistant/components/wemo/translations/ko.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { - "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + "description": "Wemo\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/wemo/translations/nl.json b/homeassistant/components/wemo/translations/nl.json index 6146072623a..e4087863726 100644 --- a/homeassistant/components/wemo/translations/nl.json +++ b/homeassistant/components/wemo/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Geen Wemo-apparaten gevonden op het netwerk.", - "single_instance_allowed": "Slechts een enkele configuratie van Wemo is mogelijk." + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 0e3c0c6e0da..6d97037a4ee 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -5,10 +5,9 @@ import logging import voluptuous as vol import whois -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_DOMAIN, CONF_NAME, TIME_DAYS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return -class WhoisSensor(Entity): +class WhoisSensor(SensorEntity): """Implementation of a WHOIS sensor.""" def __init__(self, name, domain): @@ -81,7 +80,7 @@ class WhoisSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Get the more info attributes.""" return self._attributes diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 000b961bda9..36f6e641508 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -33,11 +33,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor", "binary_sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the wiffi component. config contains data from configuration.yaml.""" - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up wiffi from a config entry, config_entry contains data from config entry database.""" if not config_entry.update_listeners: @@ -59,9 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): _LOGGER.error("Port %s already in use", config_entry.data[CONF_PORT]) raise ConfigEntryNotReady from exc - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True @@ -74,14 +69,14 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - api: "WiffiIntegrationApi" = hass.data[DOMAIN][config_entry.entry_id] + api: WiffiIntegrationApi = hass.data[DOMAIN][config_entry.entry_id] await api.server.close_server() unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py index f30ee8792df..768f66bf8de 100644 --- a/homeassistant/components/wiffi/config_flow.py +++ b/homeassistant/components/wiffi/config_flow.py @@ -11,11 +11,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.core import callback -from .const import ( # pylint: disable=unused-import - DEFAULT_PORT, - DEFAULT_TIMEOUT, - DOMAIN, -) +from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index f207e3be3ac..800a420f8f0 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + SensorEntity, ) from homeassistant.const import DEGREE, PRESSURE_MBAR, TEMP_CELSIUS from homeassistant.core import callback @@ -58,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, CREATE_ENTITY_SIGNAL, _create_entity) -class NumberEntity(WiffiEntity): +class NumberEntity(WiffiEntity, SensorEntity): """Entity for wiffi metrics which have a number value.""" def __init__(self, device, metric, options): @@ -100,7 +101,7 @@ class NumberEntity(WiffiEntity): self.async_write_ha_state() -class StringEntity(WiffiEntity): +class StringEntity(WiffiEntity, SensorEntity): """Entity for wiffi metrics which have a string value.""" def __init__(self, device, metric, options): diff --git a/homeassistant/components/wiffi/translations/hu.json b/homeassistant/components/wiffi/translations/hu.json index 21320afea78..c623f6ddaba 100644 --- a/homeassistant/components/wiffi/translations/hu.json +++ b/homeassistant/components/wiffi/translations/hu.json @@ -2,6 +2,22 @@ "config": { "abort": { "start_server_failed": "A szerver ind\u00edt\u00e1sa nem siker\u00fclt." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s (perc)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/id.json b/homeassistant/components/wiffi/translations/id.json new file mode 100644 index 00000000000..0022f83b0a1 --- /dev/null +++ b/homeassistant/components/wiffi/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "Port server sudah digunakan.", + "start_server_failed": "Gagal memulai server." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "title": "Siapkan server TCP untuk perangkat WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Tenggang waktu (menit)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/nl.json b/homeassistant/components/wiffi/translations/nl.json index af14d1942a7..966f9a18e41 100644 --- a/homeassistant/components/wiffi/translations/nl.json +++ b/homeassistant/components/wiffi/translations/nl.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "port": "Server poort" + "port": "Poort" }, "title": "TCP-server instellen voor WIFFI-apparaten" } diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 67433772551..3e14ea20b0c 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -33,9 +33,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = parent # Set up all platforms for this device/entry. - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -47,8 +47,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): # Unload entities for this entry/device. await asyncio.gather( *( - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ) ) diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 32c66df65a7..2706db07871 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components import ssdp from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow from homeassistant.const import CONF_HOST -from . import DOMAIN # pylint: disable=unused-import +from . import DOMAIN CONF_SERIAL_NUMBER = "serial_number" CONF_MODEL_NAME = "model_name" diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index d663dc39ded..e55413926ac 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -1,4 +1,5 @@ """Support for WiLight Fan.""" +from __future__ import annotations from pywilight.const import ( FAN_V1, @@ -77,11 +78,14 @@ class WiLightFan(WiLightDevice, FanEntity): return self._status.get("direction", WL_DIRECTION_OFF) != WL_DIRECTION_OFF @property - def percentage(self) -> str: + def percentage(self) -> int | None: """Return the current speed percentage.""" - if "direction" in self._status: - if self._status["direction"] == WL_DIRECTION_OFF: - return 0 + if ( + "direction" in self._status + and self._status["direction"] == WL_DIRECTION_OFF + ): + return 0 + wl_speed = self._status.get("speed") if wl_speed is None: return None @@ -95,9 +99,11 @@ class WiLightFan(WiLightDevice, FanEntity): @property def current_direction(self) -> str: """Return the current direction of the fan.""" - if "direction" in self._status: - if self._status["direction"] != WL_DIRECTION_OFF: - self._direction = self._status["direction"] + if ( + "direction" in self._status + and self._status["direction"] != WL_DIRECTION_OFF + ): + self._direction = self._status["direction"] return self._direction async def async_turn_on( @@ -118,9 +124,11 @@ class WiLightFan(WiLightDevice, FanEntity): if percentage == 0: await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) return - if "direction" in self._status: - if self._status["direction"] == WL_DIRECTION_OFF: - await self._client.set_fan_direction(self._index, self._direction) + if ( + "direction" in self._status + and self._status["direction"] == WL_DIRECTION_OFF + ): + await self._client.set_fan_direction(self._index, self._direction) wl_speed = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) await self._client.set_fan_speed(self._index, wl_speed) diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json index 3b2d79a34a7..26cdaf6a025 100644 --- a/homeassistant/components/wilight/translations/hu.json +++ b/homeassistant/components/wilight/translations/hu.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "confirm": { + "title": "WiLight" + } } } } \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/id.json b/homeassistant/components/wilight/translations/id.json new file mode 100644 index 00000000000..dae7b0bd16a --- /dev/null +++ b/homeassistant/components/wilight/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "not_supported_device": "Perangkat WiLight ini saat ini tidak didukung.", + "not_wilight_device": "Perangkat ini bukan perangkat WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Apakah Anda ingin menyiapkan WiLight {name}?\n\nIni mendukung: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/ko.json b/homeassistant/components/wilight/translations/ko.json index e18250811fc..1b53a1ba544 100644 --- a/homeassistant/components/wilight/translations/ko.json +++ b/homeassistant/components/wilight/translations/ko.json @@ -2,11 +2,14 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "not_wilight_device": "\uc774 \uc7a5\uce58\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4." + "not_supported_device": "\uc774 WiLight\ub294 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "not_wilight_device": "\uc774 \uae30\uae30\ub294 WiLight\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, + "flow_title": "WiLight: {name}", "step": { "confirm": { - "description": "WiLight {name} \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? \n\n \uc9c0\uc6d0 : {components}" + "description": "WiLight {name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\uc9c0\uc6d0 \uae30\uae30: {components}", + "title": "WiLight" } } } diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 26666bf4b15..198bddc937b 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -778,7 +778,7 @@ class WinkDevice(Entity): return self.wink.pubnub_channel is None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attributes = {} battery = self._battery_level @@ -855,9 +855,9 @@ class WinkSirenDevice(WinkDevice): return "mdi:bell-ring" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - attributes = super().device_state_attributes + attributes = super().extra_state_attributes auto_shutoff = self.wink.auto_shutoff() if auto_shutoff is not None: @@ -913,9 +913,9 @@ class WinkNimbusDialDevice(WinkDevice): return f"{self.parent.name()} dial {self.wink.index() + 1}" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - attributes = super().device_state_attributes + attributes = super().extra_state_attributes dial_attributes = self.dial_attributes() return {**attributes, **dial_attributes} diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 5c45cc7b03d..2f5ac83c6f5 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -70,6 +70,6 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanelEntity): self.wink.set_mode("away") @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return {"private": self.wink.private()} diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index 77ff464a5bf..6a5977c1dc2 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -40,9 +40,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - if sensor.capability() in SENSOR_TYPES: - add_entities([WinkBinarySensorEntity(sensor, hass)]) + if ( + _id not in hass.data[DOMAIN]["unique_ids"] + and sensor.capability() in SENSOR_TYPES + ): + add_entities([WinkBinarySensorEntity(sensor, hass)]) for key in pywink.get_keys(): _id = key.object_id() + key.name() @@ -119,18 +121,18 @@ class WinkBinarySensorEntity(WinkDevice, BinarySensorEntity): return SENSOR_TYPES.get(self.capability) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - return super().device_state_attributes + return super().extra_state_attributes class WinkSmokeDetector(WinkBinarySensorEntity): """Representation of a Wink Smoke detector.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - _attributes = super().device_state_attributes + _attributes = super().extra_state_attributes _attributes["test_activated"] = self.wink.test_activated() return _attributes @@ -139,9 +141,9 @@ class WinkHub(WinkBinarySensorEntity): """Representation of a Wink Hub.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - _attributes = super().device_state_attributes + _attributes = super().extra_state_attributes _attributes["update_needed"] = self.wink.update_needed() _attributes["firmware_version"] = self.wink.firmware_version() _attributes["pairing_mode"] = self.wink.pairing_mode() @@ -159,9 +161,9 @@ class WinkRemote(WinkBinarySensorEntity): """Representation of a Wink Lutron Connected bulb remote.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - _attributes = super().device_state_attributes + _attributes = super().extra_state_attributes _attributes["button_on_pressed"] = self.wink.button_on_pressed() _attributes["button_off_pressed"] = self.wink.button_off_pressed() _attributes["button_up_pressed"] = self.wink.button_up_pressed() @@ -178,9 +180,9 @@ class WinkButton(WinkBinarySensorEntity): """Representation of a Wink Relay button.""" @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device state attributes.""" - _attributes = super().device_state_attributes + _attributes = super().extra_state_attributes _attributes["pressed"] = self.wink.pressed() _attributes["long_pressed"] = self.wink.long_pressed() return _attributes diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 7ee05f0a729..4c783e6bde1 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -99,7 +99,7 @@ class WinkThermostat(WinkDevice, ClimateEntity): return TEMP_CELSIUS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional device state attributes.""" data = {} if self.external_temperature is not None: @@ -396,7 +396,7 @@ class WinkAC(WinkDevice, ClimateEntity): return TEMP_CELSIUS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional device state attributes.""" data = {} data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption() diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index f82b74e7712..63a67d9f1ac 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -187,9 +187,9 @@ class WinkLockDevice(WinkDevice, LockEntity): self.wink.set_alarm_mode(mode) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - super_attrs = super().device_state_attributes + super_attrs = super().extra_state_attributes sensitivity = dict_value_to_key( ALARM_SENSITIVITY_MAP, self.wink.alarm_sensitivity() ) diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py index cd3eb756fb3..f640a24def2 100644 --- a/homeassistant/components/wink/sensor.py +++ b/homeassistant/components/wink/sensor.py @@ -1,8 +1,10 @@ """Support for Wink sensors.""" +from contextlib import suppress import logging import pywink +from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEGREE, TEMP_CELSIUS from . import DOMAIN, WinkDevice @@ -17,31 +19,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - if sensor.capability() in SENSOR_TYPES: - add_entities([WinkSensorDevice(sensor, hass)]) + if ( + _id not in hass.data[DOMAIN]["unique_ids"] + and sensor.capability() in SENSOR_TYPES + ): + add_entities([WinkSensorEntity(sensor, hass)]) for eggtray in pywink.get_eggtrays(): _id = eggtray.object_id() + eggtray.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkSensorDevice(eggtray, hass)]) + add_entities([WinkSensorEntity(eggtray, hass)]) for tank in pywink.get_propane_tanks(): _id = tank.object_id() + tank.name() if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkSensorDevice(tank, hass)]) + add_entities([WinkSensorEntity(tank, hass)]) for piggy_bank in pywink.get_piggy_banks(): _id = piggy_bank.object_id() + piggy_bank.name() if _id not in hass.data[DOMAIN]["unique_ids"]: try: if piggy_bank.capability() in SENSOR_TYPES: - add_entities([WinkSensorDevice(piggy_bank, hass)]) + add_entities([WinkSensorEntity(piggy_bank, hass)]) except AttributeError: _LOGGER.info("Device is not a sensor") -class WinkSensorDevice(WinkDevice): +class WinkSensorEntity(WinkDevice, SensorEntity): """Representation of a Wink sensor.""" def __init__(self, wink, hass): @@ -83,12 +87,12 @@ class WinkSensorDevice(WinkDevice): return self._unit_of_measurement @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - super_attrs = super().device_state_attributes - try: + super_attrs = super().extra_state_attributes + + # Ignore error, this sensor isn't an eggminder + with suppress(AttributeError): super_attrs["egg_times"] = self.wink.eggs() - except AttributeError: - # Ignore error, this sensor isn't an eggminder - pass + return super_attrs diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index 2632036095a..d377ae0cddf 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -48,9 +48,9 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): self.wink.set_state(False) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - attributes = super().device_state_attributes + attributes = super().extra_state_attributes try: event = self.wink.last_event() if event is not None: diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py index 0ce31762c7a..bf5e8434746 100644 --- a/homeassistant/components/wink/water_heater.py +++ b/homeassistant/components/wink/water_heater.py @@ -66,7 +66,7 @@ class WinkWaterHeater(WinkDevice, WaterHeaterEntity): return TEMP_CELSIUS @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional device state attributes.""" data = {} data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled() diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 83e92c2250b..5da19f54dcf 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -150,7 +150,7 @@ class WirelessTagPlatform: def handle_update_tags_event(self, event): """Handle push event from wireless tag manager.""" - _LOGGER.info("push notification for update arrived: %s", event) + _LOGGER.info("Push notification for update arrived: %s", event) try: tag_id = event.data.get("id") mac = event.data.get("mac") @@ -272,7 +272,7 @@ class WirelessTagBaseSensor(Entity): self._state = self.updated_state_value() @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100), diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 2a845249028..cc0ce0cb888 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class WirelessTagSensor(WirelessTagBaseSensor): +class WirelessTagSensor(WirelessTagBaseSensor, SensorEntity): """Representation of a Sensor.""" def __init__(self, api, tag, sensor_type, config): diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index c6f420d172a..2cf6d297f12 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -3,13 +3,14 @@ Support for the Withings API. For more details about this platform, please refer to the documentation at """ +from __future__ import annotations + import asyncio -from typing import Optional, cast from aiohttp.web import Request, Response import voluptuous as vol from withings_api import WithingsAuth -from withings_api.common import NotifyAppli, enum_or_raise +from withings_api.common import NotifyAppli from homeassistant.components import webhook from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -20,7 +21,6 @@ from homeassistant.components.webhook import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType @@ -120,9 +120,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_manager = await async_get_data_manager(hass, entry) _LOGGER.debug("Confirming %s is authenticated to withings", data_manager.profile) - await data_manager.poll_data_update_coordinator.async_refresh() - if not data_manager.poll_data_update_coordinator.last_update_success: - raise ConfigEntryNotReady() + await data_manager.poll_data_update_coordinator.async_config_entry_first_refresh() webhook.async_register( hass, @@ -175,7 +173,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_webhook_handler( hass: HomeAssistant, webhook_id: str, request: Request -) -> Optional[Response]: +) -> Response | None: """Handle webhooks calls.""" # Handle http head calls to the path. # When creating a notify subscription, Withings will check that the endpoint is running by sending a HEAD request. @@ -195,9 +193,7 @@ async def async_webhook_handler( return json_message_response("Parameter appli not provided", message_code=20) try: - appli = cast( - NotifyAppli, enum_or_raise(int(params.getone("appli")), NotifyAppli) - ) + appli = NotifyAppli(int(params.getone("appli"))) except ValueError: return json_message_response("Invalid appli provided", message_code=21) diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index 136a12a0e1d..a7d0a80e8e3 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -1,5 +1,7 @@ """Sensors flow for Withings.""" -from typing import Callable, List +from __future__ import annotations + +from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, @@ -16,7 +18,7 @@ from .common import BaseWithingsSensor, async_create_entities async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" entities = await async_create_entities( diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c08ddddf4a5..c0d9bcb2599 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1,4 +1,6 @@ """Common code for Withings.""" +from __future__ import annotations + import asyncio from dataclasses import dataclass import datetime @@ -6,7 +8,7 @@ from datetime import timedelta from enum import Enum, IntEnum import logging import re -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict from aiohttp.web import Response import requests @@ -26,7 +28,7 @@ from withings_api.common import ( from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, HTTP_UNAUTHORIZED, @@ -86,7 +88,7 @@ class WithingsAttribute: measute_type: Enum friendly_name: str unit_of_measurement: str - icon: Optional[str] + icon: str | None platform: str enabled_by_default: bool update_type: UpdateType @@ -461,12 +463,12 @@ WITHINGS_ATTRIBUTES = [ ), ] -WITHINGS_MEASUREMENTS_MAP: Dict[Measurement, WithingsAttribute] = { +WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsAttribute] = { attr.measurement: attr for attr in WITHINGS_ATTRIBUTES } -WITHINGS_MEASURE_TYPE_MAP: Dict[ - Union[NotifyAppli, GetSleepSummaryField, MeasureType], WithingsAttribute +WITHINGS_MEASURE_TYPE_MAP: dict[ + NotifyAppli | GetSleepSummaryField | MeasureType, WithingsAttribute ] = {attr.measute_type: attr for attr in WITHINGS_ATTRIBUTES} @@ -486,8 +488,8 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): self.session = OAuth2Session(hass, config_entry, implementation) def _request( - self, path: str, params: Dict[str, Any], method: str = "GET" - ) -> Dict[str, Any]: + self, path: str, params: dict[str, Any], method: str = "GET" + ) -> dict[str, Any]: """Perform an async request.""" asyncio.run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self._hass.loop @@ -524,7 +526,7 @@ class WebhookUpdateCoordinator: """Initialize the object.""" self._hass = hass self._user_id = user_id - self._listeners: List[CALLBACK_TYPE] = [] + self._listeners: list[CALLBACK_TYPE] = [] self.data: MeasurementData = {} def async_add_listener(self, listener: CALLBACK_TYPE) -> Callable[[], None]: @@ -573,10 +575,8 @@ class DataManager: self._notify_unsubscribe_delay = datetime.timedelta(seconds=1) self._is_available = True - self._cancel_interval_update_interval: Optional[CALLBACK_TYPE] = None - self._cancel_configure_webhook_subscribe_interval: Optional[ - CALLBACK_TYPE - ] = None + self._cancel_interval_update_interval: CALLBACK_TYPE | None = None + self._cancel_configure_webhook_subscribe_interval: CALLBACK_TYPE | None = None self._api_notification_id = f"withings_{self._user_id}" self.subscription_update_coordinator = DataUpdateCoordinator( @@ -600,7 +600,7 @@ class DataManager: self.webhook_update_coordinator = WebhookUpdateCoordinator( self._hass, self._user_id ) - self._cancel_subscription_update: Optional[Callable[[], None]] = None + self._cancel_subscription_update: Callable[[], None] | None = None self._subscribe_webhook_run_count = 0 @property @@ -681,7 +681,7 @@ class DataManager: ) # Determine what subscriptions need to be created. - ignored_applis = frozenset({NotifyAppli.USER}) + ignored_applis = frozenset({NotifyAppli.USER, NotifyAppli.UNKNOWN}) to_add_applis = frozenset( [ appli @@ -728,7 +728,7 @@ class DataManager: self._api.notify_revoke, profile.callbackurl, profile.appli ) - async def async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]: + async def async_get_all_data(self) -> dict[MeasureType, Any] | None: """Update all withings data.""" try: return await self._do_retry(self._async_get_all_data) @@ -740,7 +740,7 @@ class DataManager: context = { const.PROFILE: self._profile, "userid": self._user_id, - "source": "reauth", + "source": SOURCE_REAUTH, } # Check if reauth flow already exists. @@ -764,14 +764,14 @@ class DataManager: raise exception - async def _async_get_all_data(self) -> Optional[Dict[MeasureType, Any]]: + async def _async_get_all_data(self) -> dict[MeasureType, Any] | None: _LOGGER.info("Updating all withings data") return { **await self.async_get_measures(), **await self.async_get_sleep_summary(), } - async def async_get_measures(self) -> Dict[MeasureType, Any]: + async def async_get_measures(self) -> dict[MeasureType, Any]: """Get the measures data.""" _LOGGER.debug("Updating withings measures") @@ -794,7 +794,7 @@ class DataManager: for measure in group.measures } - async def async_get_sleep_summary(self) -> Dict[MeasureType, Any]: + async def async_get_sleep_summary(self) -> dict[MeasureType, Any]: """Get the sleep summary data.""" _LOGGER.debug("Updating withing sleep summary") now = dt.utcnow() @@ -837,7 +837,7 @@ class DataManager: response = await self._hass.async_add_executor_job(get_sleep_summary) # Set the default to empty lists. - raw_values: Dict[GetSleepSummaryField, List[int]] = { + raw_values: dict[GetSleepSummaryField, list[int]] = { field: [] for field in GetSleepSummaryField } @@ -846,11 +846,11 @@ class DataManager: data = serie.data for field in GetSleepSummaryField: - raw_values[field].append(data._asdict()[field.value]) + raw_values[field].append(dict(data)[field.value]) - values: Dict[GetSleepSummaryField, float] = {} + values: dict[GetSleepSummaryField, float] = {} - def average(data: List[int]) -> float: + def average(data: list[int]) -> float: return sum(data) / len(data) def set_value(field: GetSleepSummaryField, func: Callable) -> None: @@ -907,7 +907,7 @@ def get_attribute_unique_id(attribute: WithingsAttribute, user_id: int) -> str: async def async_get_entity_id( hass: HomeAssistant, attribute: WithingsAttribute, user_id: int -) -> Optional[str]: +) -> str | None: """Get an entity id for a user's attribute.""" entity_registry: EntityRegistry = ( await hass.helpers.entity_registry.async_get_registry() @@ -936,7 +936,7 @@ class BaseWithingsSensor(Entity): self._user_id = self._data_manager.user_id self._name = f"Withings {self._attribute.measurement.value} {self._profile}" self._unique_id = get_attribute_unique_id(self._attribute, self._user_id) - self._state_data: Optional[Any] = None + self._state_data: Any | None = None @property def should_poll(self) -> bool: @@ -967,11 +967,6 @@ class BaseWithingsSensor(Entity): """Return a unique, Home Assistant friendly identifier for this entity.""" return self._unique_id - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return self._attribute.unit_of_measurement - @property def icon(self) -> str: """Icon to use in the frontend, if any.""" @@ -1053,7 +1048,7 @@ async def async_get_data_manager( def get_data_manager_by_webhook_id( hass: HomeAssistant, webhook_id: str -) -> Optional[DataManager]: +) -> DataManager | None: """Get a data manager by it's webhook id.""" return next( iter( @@ -1067,14 +1062,12 @@ def get_data_manager_by_webhook_id( ) -def get_all_data_managers(hass: HomeAssistant) -> Tuple[DataManager, ...]: +def get_all_data_managers(hass: HomeAssistant) -> tuple[DataManager, ...]: """Get all configured data managers.""" return tuple( - [ - config_entry_data[const.DATA_MANAGER] - for config_entry_data in hass.data[const.DOMAIN].values() - if const.DATA_MANAGER in config_entry_data - ] + config_entry_data[const.DATA_MANAGER] + for config_entry_data in hass.data[const.DOMAIN].values() + if const.DATA_MANAGER in config_entry_data ) @@ -1088,7 +1081,7 @@ async def async_create_entities( entry: ConfigEntry, create_func: Callable[[DataManager, WithingsAttribute], Entity], platform: str, -) -> List[Entity]: +) -> list[Entity]: """Create withings entities from config entry.""" data_manager = await async_get_data_manager(hass, entry) @@ -1098,14 +1091,10 @@ async def async_create_entities( ] -def get_platform_attributes(platform: str) -> Tuple[WithingsAttribute, ...]: +def get_platform_attributes(platform: str) -> tuple[WithingsAttribute, ...]: """Get withings attributes used for a specific platform.""" return tuple( - [ - attribute - for attribute in WITHINGS_ATTRIBUTES - if attribute.platform == platform - ] + attribute for attribute in WITHINGS_ATTRIBUTES if attribute.platform == platform ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index ddf51741c62..f841c61fbcb 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -1,12 +1,14 @@ """Config flow for Withings.""" +from __future__ import annotations + import logging -from typing import Dict, Union import voluptuous as vol from withings_api.common import AuthScope from homeassistant import config_entries from homeassistant.components.withings import const +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import slugify @@ -19,7 +21,7 @@ class WithingsFlowHandler( DOMAIN = const.DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL # Temporarily holds authorization data during the profile step. - _current_data: Dict[str, Union[None, str, int]] = {} + _current_data: dict[str, None | str | int] = {} @property def logger(self) -> logging.Logger: @@ -50,7 +52,7 @@ class WithingsFlowHandler( errors = {} reauth_profile = ( self.context.get(const.PROFILE) - if self.context.get("source") == "reauth" + if self.context.get("source") == SOURCE_REAUTH else None ) profile = data.get(const.PROFILE) or reauth_profile diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index ec981ff691c..6b2918722ba 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -3,7 +3,7 @@ "name": "Withings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", - "requirements": ["withings-api==2.1.6"], + "requirements": ["withings-api==2.3.2"], "dependencies": ["http", "webhook"], "codeowners": ["@vangorra"] } diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index a55d83d717b..e26804f1f0a 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -1,7 +1,9 @@ """Sensors flow for Withings.""" -from typing import Callable, List, Union +from __future__ import annotations -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from typing import Callable + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity @@ -12,7 +14,7 @@ from .common import BaseWithingsSensor, async_create_entities async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up the sensor config entry.""" @@ -26,10 +28,15 @@ async def async_setup_entry( async_add_entities(entities, True) -class WithingsHealthSensor(BaseWithingsSensor): +class WithingsHealthSensor(BaseWithingsSensor, SensorEntity): """Implementation of a Withings sensor.""" @property - def state(self) -> Union[None, str, int, float]: + def state(self) -> None | str | int | float: """Return the state of the entity.""" return self._state_data + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return self._attribute.unit_of_measurement diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index 1486048adfc..d39410b6d17 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -2,15 +2,19 @@ "config": { "abort": { "already_configured": "A profil konfigur\u00e1ci\u00f3ja friss\u00edtve.", - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, "create_entry": { "default": "A Withings sikeresen hiteles\u00edtett." }, + "error": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, "step": { "pick_implementation": { - "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" }, "profile": { "data": { @@ -18,6 +22,9 @@ }, "description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.", "title": "Felhaszn\u00e1l\u00f3i profil." + }, + "reauth": { + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } } diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json new file mode 100644 index 00000000000..e254e61d91e --- /dev/null +++ b/homeassistant/components/withings/translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Konfigurasi diperbarui untuk profil.", + "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})" + }, + "create_entry": { + "default": "Berhasil mengautentikasi dengan Withings." + }, + "error": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "flow_title": "Withings: {profile}", + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "profile": { + "data": { + "profile": "Nama Profil" + }, + "description": "Berikan nama profil unik untuk data ini. Umumnya namanya sama dengan nama profil yang Anda pilih di langkah sebelumnya.", + "title": "Profil Pengguna." + }, + "reauth": { + "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ko.json b/homeassistant/components/withings/translations/ko.json index 38ed96dca67..4823061de41 100644 --- a/homeassistant/components/withings/translations/ko.json +++ b/homeassistant/components/withings/translations/ko.json @@ -7,7 +7,7 @@ "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "create_entry": { - "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "Withings\ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -26,7 +26,7 @@ }, "reauth": { "description": "Withings \ub370\uc774\ud130\ub97c \uacc4\uc18d \uc218\uc2e0\ud558\ub824\uba74 \"{profile}\" \ud504\ub85c\ud544\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", - "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d" + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } } diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index 21b2d6b11e9..b20323347e4 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Configuratie bijgewerkt voor profiel.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" }, "create_entry": { @@ -12,19 +12,21 @@ "error": { "already_configured": "Account is al geconfigureerd" }, + "flow_title": "Withings: {profile}", "step": { "pick_implementation": { - "title": "Kies Authenticatiemethode" + "title": "Kies een authenticatie methode" }, "profile": { "data": { "profile": "Profiel" }, - "description": "Welk profiel hebt u op de website van Withings selecteren? Het is belangrijk dat de profielen overeenkomen, anders worden gegevens verkeerd gelabeld.", + "description": "Geef een unieke profielnaam op voor deze gegevens. Meestal is dit de naam van het profiel dat u in de vorige stap hebt geselecteerd.", "title": "Gebruikersprofiel." }, "reauth": { - "title": "Profiel opnieuw verifi\u00ebren" + "description": "Het \"{profile}\" profiel moet opnieuw worden geverifieerd om Withings gegevens te kunnen blijven ontvangen.", + "title": "Verifieer de integratie opnieuw" } } } diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 7cc91d32062..a54635f26b8 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,8 +1,10 @@ """Support for WLED.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -12,9 +14,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -30,25 +30,17 @@ from .const import ( ) SCAN_INTERVAL = timedelta(seconds=5) -WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) +PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the WLED components.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WLED from a config entry.""" # Create WLED instance for this entry coordinator = WLEDDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator @@ -60,9 +52,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Set up all platforms for this device/entry. - for component in WLED_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -75,8 +67,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *( - hass.config_entries.async_forward_entry_unload(entry, component) - for component in WLED_COMPONENTS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ) ) ) @@ -185,7 +177,7 @@ class WLEDDeviceEntity(WLEDEntity): """Defines a WLED device entity.""" @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return device information about this WLED device.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 5915447f2f5..a85a74fa94b 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure the WLED integration.""" -from typing import Any, Dict, Optional +from __future__ import annotations + +from typing import Any import voluptuous as vol from wled import WLED, WLEDConnectionError @@ -13,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): @@ -23,14 +25,14 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL async def async_step_user( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" return await self._handle_config_flow(user_input) async def async_step_zeroconf( - self, user_input: Optional[ConfigType] = None - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None + ) -> dict[str, Any]: """Handle zeroconf discovery.""" if user_input is None: return self.async_abort(reason="cannot_connect") @@ -53,13 +55,13 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: ConfigType = None - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Handle a flow initiated by zeroconf.""" return await self._handle_config_flow(user_input) async def _handle_config_flow( - self, user_input: Optional[ConfigType] = None, prepare: bool = False - ) -> Dict[str, Any]: + self, user_input: ConfigType | None = None, prepare: bool = False + ) -> dict[str, Any]: """Config flow handler for WLED.""" source = self.context.get("source") @@ -100,7 +102,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + def _show_setup_form(self, errors: dict | None = None) -> dict[str, Any]: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -108,7 +110,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + def _show_confirm_dialog(self, errors: dict | None = None) -> dict[str, Any]: """Show the confirm dialog to the user.""" name = self.context.get(CONF_NAME) return self.async_show_form( diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index f89cf06a44c..9de7eafc042 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -1,6 +1,8 @@ """Support for LED lights.""" +from __future__ import annotations + from functools import partial -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable import voluptuous as vol @@ -51,7 +53,7 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up WLED light based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -115,7 +117,7 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of this light between 1..255.""" return self.coordinator.data.state.brightness @@ -149,6 +151,27 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): await self.coordinator.wled.master(**data) + async def async_effect( + self, + effect: int | str | None = None, + intensity: int | None = None, + palette: int | str | None = None, + reverse: bool | None = None, + speed: int | None = None, + ) -> None: + """Set the effect of a WLED light.""" + # Master light does not have an effect setting. + + @wled_exception_handler + async def async_preset( + self, + preset: int, + ) -> None: + """Set a WLED light to a saved preset.""" + data = {ATTR_PRESET: preset} + + await self.coordinator.wled.preset(**data) + class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): """Defines a WLED light based on a segment.""" @@ -188,7 +211,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): return super().available @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" playlist = self.coordinator.data.state.playlist if playlist == -1: @@ -209,18 +232,18 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): } @property - def hs_color(self) -> Optional[Tuple[float, float]]: + def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" color = self.coordinator.data.state.segments[self._segment].color_primary return color_util.color_RGB_to_hs(*color[:3]) @property - def effect(self) -> Optional[str]: + def effect(self) -> str | None: """Return the current effect of the light.""" return self.coordinator.data.state.segments[self._segment].effect.name @property - def brightness(self) -> Optional[int]: + def brightness(self) -> int | None: """Return the brightness of this light between 1..255.""" state = self.coordinator.data.state @@ -234,7 +257,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): return state.segments[self._segment].brightness @property - def white_value(self) -> Optional[int]: + def white_value(self) -> int | None: """Return the white value of this light between 0..255.""" color = self.coordinator.data.state.segments[self._segment].color_primary return color[-1] if self._rgbw else None @@ -256,7 +279,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): return flags @property - def effect_list(self) -> List[str]: + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return [effect.name for effect in self.coordinator.data.effects] @@ -357,11 +380,11 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_effect( self, - effect: Optional[Union[int, str]] = None, - intensity: Optional[int] = None, - palette: Optional[Union[int, str]] = None, - reverse: Optional[bool] = None, - speed: Optional[int] = None, + effect: int | str | None = None, + intensity: int | None = None, + palette: int | str | None = None, + reverse: bool | None = None, + speed: int | None = None, ) -> None: """Set the effect of a WLED light.""" data = {ATTR_SEGMENT_ID: self._segment} @@ -398,7 +421,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): def async_update_segments( entry: ConfigEntry, coordinator: WLEDDataUpdateCoordinator, - current: Dict[int, WLEDSegmentLight], + current: dict[int, WLEDSegmentLight], async_add_entities, ) -> None: """Update segments.""" @@ -438,7 +461,7 @@ def async_update_segments( async def async_remove_entity( index: int, coordinator: WLEDDataUpdateCoordinator, - current: Dict[int, WLEDSegmentLight], + current: dict[int, WLEDSegmentLight], ) -> None: """Remove WLED segment light from Home Assistant.""" entity = current[index] diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 89d76776a82..7e91f81dea0 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -1,8 +1,10 @@ """Support for WLED sensors.""" -from datetime import timedelta -from typing import Any, Callable, Dict, List, Optional +from __future__ import annotations -from homeassistant.components.sensor import DEVICE_CLASS_CURRENT +from datetime import timedelta +from typing import Any, Callable + +from homeassistant.components.sensor import DEVICE_CLASS_CURRENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, @@ -22,7 +24,7 @@ from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up WLED sensor based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -40,7 +42,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class WLEDSensor(WLEDDeviceEntity): +class WLEDSensor(WLEDDeviceEntity, SensorEntity): """Defines a WLED sensor.""" def __init__( @@ -52,7 +54,7 @@ class WLEDSensor(WLEDDeviceEntity): icon: str, key: str, name: str, - unit_of_measurement: Optional[str] = None, + unit_of_measurement: str | None = None, ) -> None: """Initialize WLED sensor.""" self._unit_of_measurement = unit_of_measurement @@ -92,7 +94,7 @@ class WLEDEstimatedCurrentSensor(WLEDSensor): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return { ATTR_LED_COUNT: self.coordinator.data.info.leds.count, @@ -105,7 +107,7 @@ class WLEDEstimatedCurrentSensor(WLEDSensor): return self.coordinator.data.info.leds.power @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this sensor.""" return DEVICE_CLASS_CURRENT @@ -131,7 +133,7 @@ class WLEDUptimeSensor(WLEDSensor): return uptime.replace(microsecond=0).isoformat() @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this sensor.""" return DEVICE_CLASS_TIMESTAMP @@ -199,7 +201,7 @@ class WLEDWifiRSSISensor(WLEDSensor): return self.coordinator.data.info.wifi.rssi @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this sensor.""" return DEVICE_CLASS_SIGNAL_STRENGTH diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index d6927610a47..3ade18cb70e 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -2,6 +2,9 @@ effect: name: Set effect description: Control the effect settings of WLED. target: + entity: + integration: wled + domain: light fields: effect: name: Effect @@ -48,6 +51,9 @@ preset: name: Set preset description: Set a preset for the WLED device. target: + entity: + integration: wled + domain: light fields: preset: name: Preset ID diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 38ebd0e9b29..5902cd246a0 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -1,5 +1,7 @@ """Support for WLED switches.""" -from typing import Any, Callable, Dict, List, Optional +from __future__ import annotations + +from typing import Any, Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -21,7 +23,7 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up WLED switch based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -72,7 +74,7 @@ class WLEDNightlightSwitch(WLEDSwitch): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return { ATTR_DURATION: self.coordinator.data.state.nightlight.duration, @@ -110,7 +112,7 @@ class WLEDSyncSendSwitch(WLEDSwitch): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return {ATTR_UDP_PORT: self.coordinator.data.info.udp_port} @@ -144,7 +146,7 @@ class WLEDSyncReceiveSwitch(WLEDSwitch): ) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" return {ATTR_UDP_PORT: self.coordinator.data.info.udp_port} diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index b89bd72f704..0d2c85e477d 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { diff --git a/homeassistant/components/wled/translations/id.json b/homeassistant/components/wled/translations/id.json new file mode 100644 index 00000000000..6437dfaf83e --- /dev/null +++ b/homeassistant/components/wled/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Siapkan WLED Anda untuk diintegrasikan dengan Home Assistant." + }, + "zeroconf_confirm": { + "description": "Ingin menambahkan WLED `{name}` ke Home Assistant?", + "title": "Peranti WLED yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ko.json b/homeassistant/components/wled/translations/ko.json index d1945707b6d..69f30eb7516 100644 --- a/homeassistant/components/wled/translations/ko.json +++ b/homeassistant/components/wled/translations/ko.json @@ -13,10 +13,10 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4." + "description": "Home Assistant\uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4." }, "zeroconf_confirm": { - "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Home Assistant\uc5d0 WLED `{name}`\uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "\ubc1c\uacac\ub41c WLED \uae30\uae30" } } diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index 329716e2cd5..3e7b16a7f4a 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dit WLED-apparaat is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken" }, "error": { @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hostnaam of IP-adres" + "host": "Host" }, "description": "Stel uw WLED-integratie in met Home Assistant." }, diff --git a/homeassistant/components/wolflink/config_flow.py b/homeassistant/components/wolflink/config_flow.py index f54789cef78..20dbd8ef9b7 100644 --- a/homeassistant/components/wolflink/config_flow.py +++ b/homeassistant/components/wolflink/config_flow.py @@ -9,12 +9,7 @@ from wolf_smartset.wolf_client import WolfClient from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import ( # pylint:disable=unused-import - DEVICE_GATEWAY, - DEVICE_ID, - DEVICE_NAME, - DOMAIN, -) +from .const import DEVICE_GATEWAY, DEVICE_ID, DEVICE_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index 201979d4dc3..f243160ff59 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -9,6 +9,7 @@ from wolf_smartset.models import ( Temperature, ) +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -46,7 +47,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class WolfLinkSensor(CoordinatorEntity): +class WolfLinkSensor(CoordinatorEntity, SensorEntity): """Base class for all Wolf entities.""" def __init__(self, coordinator, wolf_object: Parameter, device_id): @@ -69,7 +70,7 @@ class WolfLinkSensor(CoordinatorEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { "parameter_id": self.wolf_object.parameter_id, diff --git a/homeassistant/components/wolflink/translations/hu.json b/homeassistant/components/wolflink/translations/hu.json index 3b2d79a34a7..c7bb483155d 100644 --- a/homeassistant/components/wolflink/translations/hu.json +++ b/homeassistant/components/wolflink/translations/hu.json @@ -2,6 +2,24 @@ "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" + }, + "step": { + "device": { + "data": { + "device_name": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/id.json b/homeassistant/components/wolflink/translations/id.json new file mode 100644 index 00000000000..64d692efa7d --- /dev/null +++ b/homeassistant/components/wolflink/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "device": { + "data": { + "device_name": "Perangkat" + }, + "title": "Pilih perangkat WOLF" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "title": "Koneksi WOLF SmartSet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/nl.json b/homeassistant/components/wolflink/translations/nl.json index 7fb1b867cdd..069ed928962 100644 --- a/homeassistant/components/wolflink/translations/nl.json +++ b/homeassistant/components/wolflink/translations/nl.json @@ -4,15 +4,23 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { + "device": { + "data": { + "device_name": "Apparaat" + }, + "title": "Selecteer WOLF-apparaat" + }, "user": { "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "title": "WOLF SmartSet-verbinding" } } } diff --git a/homeassistant/components/wolflink/translations/ru.json b/homeassistant/components/wolflink/translations/ru.json index 0b105ad922a..8a430c9a63d 100644 --- a/homeassistant/components/wolflink/translations/ru.json +++ b/homeassistant/components/wolflink/translations/ru.json @@ -18,7 +18,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d" + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "title": "WOLF SmartSet" } diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 9680716cd19..17c365e88c4 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -1,6 +1,7 @@ { "state": { "wolflink__state": { + "partymodus": "Party-Modus", "permanent": "Permanent", "solarbetrieb": "Solarmodus", "sparbetrieb": "Sparmodus", diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json new file mode 100644 index 00000000000..2d8cdda9315 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.hu.json @@ -0,0 +1,7 @@ +{ + "state": { + "wolflink__state": { + "permanent": "\u00c1lland\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json new file mode 100644 index 00000000000..12b755a9c24 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -0,0 +1,47 @@ +{ + "state": { + "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "aktiviert": "Diaktifkan", + "antilegionellenfunktion": "Fungsi Anti-legionella", + "aus": "Dinonaktifkan", + "auto": "Otomatis", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "Otomatis MATI", + "automatik_ein": "Otomatis NYALA", + "bereit_keine_ladung": "Siap, tidak memuat", + "betrieb_ohne_brenner": "Bekerja tanpa pembakar", + "cooling": "Mendinginkan", + "deaktiviert": "Tidak aktif", + "dhw_prior": "DHWPrior", + "eco": "Eco", + "ein": "Diaktifkan", + "externe_deaktivierung": "Penonaktifan eksternal", + "fernschalter_ein": "Kontrol jarak jauh diaktifkan", + "heizung": "Memanaskan", + "initialisierung": "Inisialisasi", + "kalibration": "Kalibrasi", + "kalibration_heizbetrieb": "Kalibrasi mode pemanasan", + "kalibration_kombibetrieb": "Kalibrasi mode kombi", + "kalibration_warmwasserbetrieb": "Kalibrasi DHW", + "kaskadenbetrieb": "Operasi bertingkat", + "kombibetrieb": "Mode kombi", + "mindest_kombizeit": "Waktu kombi minimum", + "nur_heizgerat": "Hanya boiler", + "parallelbetrieb": "Mode paralel", + "partymodus": "Mode pesta", + "permanent": "Permanen", + "permanentbetrieb": "Mode permanen", + "reduzierter_betrieb": "Mode terbatas", + "schornsteinfeger": "Uji emisi", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", + "solarbetrieb": "Mode surya", + "sparbetrieb": "Mode ekonomi", + "sparen": "Ekonomi", + "urlaubsmodus": "Mode liburan", + "ventilprufung": "Uji katup" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.ko.json b/homeassistant/components/wolflink/translations/sensor.ko.json index 71fde05a07a..2597560be1b 100644 --- a/homeassistant/components/wolflink/translations/sensor.ko.json +++ b/homeassistant/components/wolflink/translations/sensor.ko.json @@ -8,14 +8,80 @@ "aktiviert": "\ud65c\uc131\ud654", "antilegionellenfunktion": "\ud56d \ub808\uc9c0\uc624\ub12c\ub77c\uade0 \uae30\ub2a5", "at_abschaltung": "OT \ub044\uae30", - "at_frostschutz": "OT \ube59\uacb0 \ubcf4\ud638", + "at_frostschutz": "OT \ub3d9\ud30c \ubc29\uc9c0", "aus": "\ube44\ud65c\uc131\ud654", "auto": "\uc790\ub3d9", "auto_off_cool": "\ub0c9\ubc29 \uc790\ub3d9 \uaebc\uc9d0", "auto_on_cool": "\ub0c9\ubc29 \uc790\ub3d9 \ucf1c\uc9d0", "automatik_aus": "\uc790\ub3d9 \uaebc\uc9d0", "automatik_ein": "\uc790\ub3d9 \ucf1c\uc9d0", - "permanent": "\uc601\uad6c\uc801" + "bereit_keine_ladung": "\uc900\ube44\ub428, \ub85c\ub4dc\ub418\uc9c0\ub294 \uc54a\uc74c", + "betrieb_ohne_brenner": "\ubc84\ub108 \uc5c6\uc774 \uc791\ub3d9", + "cooling": "\ub0c9\ubc29", + "deaktiviert": "\ube44\ud65c\uc131", + "dhw_prior": "DHW \uc6b0\uc120", + "eco": "\uc808\uc57d", + "ein": "\ud65c\uc131\ud654", + "estrichtrocknung": "\uc7a5\uae30\uac04 \uc81c\uc2b5", + "externe_deaktivierung": "\uc678\ubd80 \ube44\ud65c\uc131\ud654", + "fernschalter_ein": "\uc6d0\uaca9 \uc81c\uc5b4 \ud65c\uc131\ud654", + "frost_heizkreis": "\ub09c\ubc29 \ud68c\ub85c \ub3d9\ud30c", + "frost_warmwasser": "DHW \ub3d9\ud30c", + "frostschutz": "\ub3d9\ud30c \ubc29\uc9c0", + "gasdruck": "\uac00\uc2a4 \uc555\ub825", + "glt_betrieb": "BMS \ubaa8\ub4dc", + "gradienten_uberwachung": "\uae30\uc6b8\uae30 \ubaa8\ub2c8\ud130\ub9c1", + "heizbetrieb": "\ub09c\ubc29 \ubaa8\ub4dc", + "heizgerat_mit_speicher": "\uc2e4\ub9b0\ub354 \ubcf4\uc77c\ub7ec", + "heizung": "\ub09c\ubc29", + "initialisierung": "\ucd08\uae30\ud654", + "kalibration": "\ubcf4\uc815", + "kalibration_heizbetrieb": "\ub09c\ubc29 \ubaa8\ub4dc \ubcf4\uc815", + "kalibration_kombibetrieb": "\ucf64\ube44 \ubaa8\ub4dc \ubcf4\uc815", + "kalibration_warmwasserbetrieb": "DHW \ubcf4\uc815", + "kaskadenbetrieb": "\uce90\uc2a4\ucf00\uc774\ub4dc \uc6b4\uc804", + "kombibetrieb": "\ucf64\ube44 \ubaa8\ub4dc", + "kombigerat": "\ucf64\ube44 \ubcf4\uc77c\ub7ec", + "kombigerat_mit_solareinbindung": "\ud0dc\uc591\uc5f4 \ud1b5\ud569\ud615 \ucf64\ube44 \ubcf4\uc77c\ub7ec", + "mindest_kombizeit": "\ucd5c\uc18c \ucf64\ube44 \uc2dc\uac04", + "nachlauf_heizkreispumpe": "\ub09c\ubc29 \ud68c\ub85c \ud38c\ud504 \uc6b4\uc804", + "nachspulen": "\ud50c\ub7ec\uc2dc \ud6c4", + "nur_heizgerat": "\ubcf4\uc77c\ub7ec\ub9cc", + "parallelbetrieb": "\ubcd1\ub82c \ubaa8\ub4dc", + "partymodus": "\ud30c\ud2f0 \ubaa8\ub4dc", + "perm_cooling": "\uc601\uad6c \ub0c9\ubc29", + "permanent": "\uc601\uad6c", + "permanentbetrieb": "\uc601\uad6c \ubaa8\ub4dc", + "reduzierter_betrieb": "\uc81c\ud55c \ubaa8\ub4dc", + "rt_abschaltung": "RT \ub044\uae30", + "rt_frostschutz": "RT \ub3d9\ud30c \ubcf4\ud638", + "ruhekontakt": "\uc0c1\uc2dc \ud3d0\uc1c4(NC)", + "schornsteinfeger": "\ubc30\uae30 \ud14c\uc2a4\ud2b8", + "smart_grid": "\uc2a4\ub9c8\ud2b8\uadf8\ub9ac\ub4dc", + "smart_home": "\uc2a4\ub9c8\ud2b8\ud648", + "softstart": "\uc18c\ud504\ud2b8 \uc2a4\ud0c0\ud2b8", + "solarbetrieb": "\ud0dc\uc591\uc5f4 \ubaa8\ub4dc", + "sparbetrieb": "\uc808\uc57d \ubaa8\ub4dc", + "sparen": "\uc808\uc57d", + "spreizung_hoch": "\ub108\ubb34 \ub113\uc740 dT", + "spreizung_kf": "KF \ud655\uc0b0", + "stabilisierung": "\uc548\uc815\ud654", + "standby": "\uc900\ube44\uc911", + "start": "\uc2dc\uc791", + "storung": "\uc624\ub958", + "taktsperre": "\uc21c\ud658 \ubc29\uc9c0", + "telefonfernschalter": "\uc804\ud654 \uc6d0\uaca9 \uc2a4\uc704\uce58", + "test": "\ud14c\uc2a4\ud2b8", + "tpw": "TPW", + "urlaubsmodus": "\ud734\uc77c \ubaa8\ub4dc", + "ventilprufung": "\ubc38\ube0c \ud14c\uc2a4\ud2b8", + "vorspulen": "\uc785\uc218\uad6c \uccad\uc18c", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW \ube60\ub978 \uc2dc\uc791", + "warmwasserbetrieb": "DHW \ubaa8\ub4dc", + "warmwassernachlauf": "DHW \uc6b4\uc804", + "warmwasservorrang": "DHW \uc6b0\uc120\uc21c\uc704", + "zunden": "\uc810\ud654" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.nl.json b/homeassistant/components/wolflink/translations/sensor.nl.json index ae205d79aef..f050fe4f629 100644 --- a/homeassistant/components/wolflink/translations/sensor.nl.json +++ b/homeassistant/components/wolflink/translations/sensor.nl.json @@ -1,25 +1,87 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "Rookgasklep", + "absenkbetrieb": "Setback modus", + "absenkstop": "Setback stop", + "aktiviert": "Geactiveerd", + "antilegionellenfunktion": "Anti-legionella functie", + "at_abschaltung": "OT afsluiten", + "at_frostschutz": "OT vorstbescherming", + "aus": "Uitgeschakeld", + "auto": "Auto", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "Automatisch UIT", + "automatik_ein": "Automatisch AAN", + "bereit_keine_ladung": "Klaar, niet laden", + "betrieb_ohne_brenner": "Werken zonder brander", + "cooling": "Koelen", + "deaktiviert": "Inactief", + "dhw_prior": "DHWPrior", + "eco": "Eco", + "ein": "Ingeschakeld", + "estrichtrocknung": "Dekvloer drogen", + "externe_deaktivierung": "Externe uitschakeling", + "fernschalter_ein": "Op afstand bedienen ingeschakeld", + "frost_heizkreis": "Verwarmengscircuit ontdooien", "frost_warmwasser": "DHW vorst", "frostschutz": "Vorstbescherming", "gasdruck": "Gasdruk", "glt_betrieb": "BMS-modus", + "gradienten_uberwachung": "Gradient monitoring", "heizbetrieb": "Verwarmingsmodus", "heizgerat_mit_speicher": "Boiler met cilinder", "heizung": "Verwarmen", "initialisierung": "Initialisatie", "kalibration": "Kalibratie", "kalibration_heizbetrieb": "Kalibratie verwarmingsmodus", + "kalibration_kombibetrieb": "Kalibratie van de combimodus", + "kalibration_warmwasserbetrieb": "DHW-kalibratie", + "kaskadenbetrieb": "Cascade operation", + "kombibetrieb": "Combi-modus", + "kombigerat": "Combiketel", + "kombigerat_mit_solareinbindung": "Combiketel met zonne-integratie", + "mindest_kombizeit": "Minimale combitijd", + "nachlauf_heizkreispumpe": "De pomp van het verwarmingscircuit gaat aan", + "nachspulen": "Post-flush", + "nur_heizgerat": "Alleen ketel", + "parallelbetrieb": "Parallelle modus", + "partymodus": "Feestmodus", + "perm_cooling": "PermCooling", "permanent": "Permanent", + "permanentbetrieb": "Permanente modus", + "reduzierter_betrieb": "Beperkte modus", + "rt_abschaltung": "RT afsluiten", + "rt_frostschutz": "RT vorstbescherming", + "ruhekontakt": "Rest contact", + "schornsteinfeger": "Emissietest", + "smart_grid": "SmartGrid", + "smart_home": "SmartHome", + "softstart": "Zachte start", + "solarbetrieb": "Zonnemodus", + "sparbetrieb": "Spaarstand", + "sparen": "Spaarstand", + "spreizung_hoch": "dT te breed", + "spreizung_kf": "Spreid KF", + "stabilisierung": "Stablisatie", "standby": "Stand-by", "start": "Start", "storung": "Fout", + "taktsperre": "Anti-cyclus", + "telefonfernschalter": "Telefoon schakelaar op afstand", "test": "Test", "tpw": "TPW", "urlaubsmodus": "Vakantiemodus", "ventilprufung": "Kleptest", - "warmwasser": "DHW" + "vorspulen": "Invoer spoelen", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW Snel starten", + "warmwasserbetrieb": "DHW-modus", + "warmwassernachlauf": "DHW aanloop", + "warmwasservorrang": "DHW prioriteit", + "zunden": "Ontsteking" } } } \ No newline at end of file diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 44f30cf9538..ed3822b9698 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,5 +1,5 @@ """Sensor to indicate whether the current day is a workday.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from typing import Any @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_NAME, WEEKDAYS import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt _LOGGER = logging.getLogger(__name__) @@ -77,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_name = config[CONF_NAME] workdays = config[CONF_WORKDAYS] - year = (get_date(datetime.today()) + timedelta(days=days_offset)).year + year = (get_date(dt.now()) + timedelta(days=days_offset)).year obj_holidays = getattr(holidays, country)(years=year) if province: @@ -170,7 +171,7 @@ class IsWorkdaySensor(BinarySensorEntity): return False @property - def state_attributes(self): + def extra_state_attributes(self): """Return the attributes of the entity.""" # return self._attributes return { @@ -185,7 +186,7 @@ class IsWorkdaySensor(BinarySensorEntity): self._state = False # Get ISO day of the week (1 = Monday, 7 = Sunday) - date = get_date(datetime.today()) + timedelta(days=self._days_offset) + date = get_date(dt.now()) + timedelta(days=self._days_offset) day = date.isoweekday() - 1 day_of_week = day_to_string(day) diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index e02dc3a0d5c..de5b3991e3f 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -1,10 +1,9 @@ """Support for showing the time in a different time zone.""" import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME, CONF_TIME_ZONE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util CONF_TIME_FORMAT = "time_format" @@ -39,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class WorldClockSensor(Entity): +class WorldClockSensor(SensorEntity): """Representation of a World clock sensor.""" def __init__(self, time_zone, name, time_format): diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index aaa9f2d1585..0fa65957e40 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -6,7 +6,7 @@ import time import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_NAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -55,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([tides]) -class WorldTidesInfoSensor(Entity): +class WorldTidesInfoSensor(SensorEntity): """Representation of a WorldTidesInfo sensor.""" def __init__(self, name, lat, lon, key): @@ -72,7 +71,7 @@ class WorldTidesInfoSensor(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of this device.""" attr = {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index e4fe33f62f1..e7600670c52 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -6,11 +6,10 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT, PERCENTAGE from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -50,7 +49,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([WorxLandroidSensor(typ, config)]) -class WorxLandroidSensor(Entity): +class WorxLandroidSensor(SensorEntity): """Implementation of a Worx Landroid sensor.""" def __init__(self, sensor, config): @@ -128,9 +127,8 @@ class WorxLandroidSensor(Entity): def get_error(obj): """Get the mower error.""" for i, err in enumerate(obj["allarmi"]): - if i != 2: # ignore wire bounce errors - if err == 1: - return ERROR_STATE[i] + if i != 2 and err == 1: # ignore wire bounce errors + return ERROR_STATE[i] return None diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 786fd07f626..9e4d957d028 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -6,7 +6,7 @@ import re import requests import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_NAME, @@ -17,7 +17,6 @@ from homeassistant.const import ( TIME_MINUTES, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -65,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class WashingtonStateTransportSensor(Entity): +class WashingtonStateTransportSensor(SensorEntity): """ Sensor that reads the WSDOT web API. @@ -120,7 +119,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): self._state = self._data.get(ATTR_CURRENT_TIME) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index e1bd79b7ea0..358e305dc47 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -1,16 +1,18 @@ """Support for WUnderground weather service.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging import re -from typing import Any, Callable, Optional, Union +from typing import Any, Callable import aiohttp import async_timeout import voluptuous as vol from homeassistant.components import sensor -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -34,7 +36,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle @@ -64,13 +65,13 @@ class WUSensorConfig: def __init__( self, - friendly_name: Union[str, Callable], + friendly_name: str | Callable, feature: str, - value: Callable[["WUndergroundData"], Any], - unit_of_measurement: Optional[str] = None, + value: Callable[[WUndergroundData], Any], + unit_of_measurement: str | None = None, entity_picture=None, icon: str = "mdi:gauge", - device_state_attributes=None, + extra_state_attributes=None, device_class=None, ): """Initialize sensor configuration. @@ -82,7 +83,7 @@ class WUSensorConfig: :param unit_of_measurement: unit of measurement :param entity_picture: value or callback returning URL of entity picture :param icon: icon name or URL - :param device_state_attributes: dictionary of attributes, or callable that returns it + :param extra_state_attributes: dictionary of attributes, or callable that returns it """ self.friendly_name = friendly_name self.unit_of_measurement = unit_of_measurement @@ -90,7 +91,7 @@ class WUSensorConfig: self.value = value self.entity_picture = entity_picture self.icon = icon - self.device_state_attributes = device_state_attributes or {} + self.extra_state_attributes = extra_state_attributes or {} self.device_class = device_class @@ -99,10 +100,10 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): def __init__( self, - friendly_name: Union[str, Callable], + friendly_name: str | Callable, field: str, - icon: Optional[str] = "mdi:gauge", - unit_of_measurement: Optional[str] = None, + icon: str | None = "mdi:gauge", + unit_of_measurement: str | None = None, device_class=None, ): """Initialize current conditions sensor configuration. @@ -121,7 +122,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): entity_picture=lambda wu: wu.data["current_observation"]["icon_url"] if icon is None else None, - device_state_attributes={ + extra_state_attributes={ "date": lambda wu: wu.data["current_observation"]["observation_time"] }, device_class=device_class, @@ -131,9 +132,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): class WUDailyTextForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for daily text forecasts.""" - def __init__( - self, period: int, field: str, unit_of_measurement: Optional[str] = None - ): + def __init__(self, period: int, field: str, unit_of_measurement: str | None = None): """Initialize daily text forecast sensor configuration. :param period: forecast period number @@ -152,7 +151,7 @@ class WUDailyTextForecastSensorConfig(WUSensorConfig): "forecastday" ][period]["icon_url"], unit_of_measurement=unit_of_measurement, - device_state_attributes={ + extra_state_attributes={ "date": lambda wu: wu.data["forecast"]["txt_forecast"]["date"] }, ) @@ -166,8 +165,8 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): friendly_name: str, period: int, field: str, - wu_unit: Optional[str] = None, - ha_unit: Optional[str] = None, + wu_unit: str | None = None, + ha_unit: str | None = None, icon=None, device_class=None, ): @@ -201,7 +200,7 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): if not icon else None, icon=icon, - device_state_attributes={ + extra_state_attributes={ "date": lambda wu: wu.data["forecast"]["simpleforecast"]["forecastday"][ period ]["date"]["pretty"] @@ -227,7 +226,7 @@ class WUHourlyForecastSensorConfig(WUSensorConfig): feature="hourly", value=lambda wu: wu.data["hourly_forecast"][period][field], entity_picture=lambda wu: wu.data["hourly_forecast"][period]["icon_url"], - device_state_attributes={ + extra_state_attributes={ "temp_c": lambda wu: wu.data["hourly_forecast"][period]["temp"][ "metric" ], @@ -273,7 +272,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): def __init__( self, - friendly_name: Union[str, Callable], + friendly_name: str | Callable, field: str, value_type: str, wu_unit: str, @@ -303,7 +302,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" - def __init__(self, friendly_name: Union[str, Callable]): + def __init__(self, friendly_name: str | Callable): """Initialiize alerts sensor configuration. :param friendly_name: Friendly name @@ -315,7 +314,7 @@ class WUAlertsSensorConfig(WUSensorConfig): icon=lambda wu: "mdi:alert-circle-outline" if wu.data["alerts"] else "mdi:check-circle-outline", - device_state_attributes=self._get_attributes, + extra_state_attributes=self._get_attributes, ) @staticmethod @@ -1117,7 +1116,7 @@ async def async_setup_platform( async_add_entities(sensors, True) -class WUndergroundSensor(Entity): +class WUndergroundSensor(SensorEntity): """Implementing the WUnderground sensor.""" def __init__(self, hass: HomeAssistantType, rest, condition, unique_id_base: str): @@ -1157,7 +1156,7 @@ class WUndergroundSensor(Entity): def _update_attrs(self): """Parse and update device state attributes.""" - attrs = self._cfg_expand("device_state_attributes", {}) + attrs = self._cfg_expand("extra_state_attributes", {}) for (attr, callback) in attrs.items(): if callable(callback): @@ -1185,7 +1184,7 @@ class WUndergroundSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index 6373cfa7535..58ce7587070 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -9,6 +9,7 @@ import xbee_helper.const as xb_const from xbee_helper.device import convert_adc from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, @@ -365,7 +366,7 @@ class XBeeDigitalOut(XBeeDigitalIn): self._state = self._config.state2bool[pin_state] -class XBeeAnalogIn(Entity): +class XBeeAnalogIn(SensorEntity): """Representation of a GPIO pin configured as an analog input.""" def __init__(self, config, device): diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py index 4d9f9ca518b..78cfe964277 100644 --- a/homeassistant/components/xbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -5,8 +5,8 @@ import logging import voluptuous as vol from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure +from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_TYPE, TEMP_CELSIUS -from homeassistant.helpers.entity import Entity from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig @@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([sensor_class(config_class(config), zigbee_device)], True) -class XBeeTemperatureSensor(Entity): +class XBeeTemperatureSensor(SensorEntity): """Representation of XBee Pro temperature sensor.""" def __init__(self, config, device): diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 3e8d537799a..d287e515cef 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -1,9 +1,11 @@ """The xbox integration.""" +from __future__ import annotations + import asyncio +from contextlib import suppress from dataclasses import dataclass from datetime import timedelta import logging -from typing import Dict, Optional import voluptuous as vol from xbox.webapi.api.client import XboxLiveClient @@ -93,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) coordinator = XboxUpdateCoordinator(hass, client, consoles) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { "client": XboxLiveClient(auth), @@ -101,9 +103,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "coordinator": coordinator, } - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -114,8 +116,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -133,7 +135,7 @@ class ConsoleData: """Xbox console status data.""" status: SmartglassConsoleStatus - app_details: Optional[Product] + app_details: Product | None @dataclass @@ -149,7 +151,7 @@ class PresenceData: in_game: bool in_multiplayer: bool gamer_score: str - gold_tenure: Optional[str] + gold_tenure: str | None account_tier: str @@ -157,8 +159,8 @@ class PresenceData: class XboxData: """Xbox dataclass for update coordinator.""" - consoles: Dict[str, ConsoleData] - presence: Dict[str, PresenceData] + consoles: dict[str, ConsoleData] + presence: dict[str, PresenceData] class XboxUpdateCoordinator(DataUpdateCoordinator): @@ -184,9 +186,9 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> XboxData: """Fetch the latest console status.""" # Update Console Status - new_console_data: Dict[str, ConsoleData] = {} + new_console_data: dict[str, ConsoleData] = {} for console in self.consoles.result: - current_state: Optional[ConsoleData] = self.data.consoles.get(console.id) + current_state: ConsoleData | None = self.data.consoles.get(console.id) status: SmartglassConsoleStatus = ( await self.client.smartglass.get_console_status(console.id) ) @@ -198,7 +200,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): ) # Setup focus app - app_details: Optional[Product] = None + app_details: Product | None = None if current_state is not None: app_details = current_state.app_details @@ -246,13 +248,11 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): def _build_presence_data(person: Person) -> PresenceData: """Build presence data from a person.""" - active_app: Optional[PresenceDetail] = None - try: + active_app: PresenceDetail | None = None + with suppress(StopIteration): active_app = next( presence for presence in person.presence_details if presence.is_primary ) - except StopIteration: - pass return PresenceData( xuid=person.xuid, diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 028f1d4c9ec..c149ce74c32 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -1,5 +1,7 @@ """Base Sensor for the Xbox Integration.""" -from typing import Optional +from __future__ import annotations + +from yarl import URL from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -22,7 +24,7 @@ class XboxBaseSensorEntity(CoordinatorEntity): return f"{self.xuid}_{self.attribute}" @property - def data(self) -> Optional[PresenceData]: + def data(self) -> PresenceData | None: """Return coordinator data for this console.""" return self.coordinator.data.presence.get(self.xuid) @@ -44,7 +46,17 @@ class XboxBaseSensorEntity(CoordinatorEntity): if not self.data: return None - return self.data.display_pic.replace("&mode=Padding", "") + # Xbox sometimes returns a domain that uses a wrong certificate which creates issues + # with loading the image. + # The correct domain is images-eds-ssl which can just be replaced + # to point to the correct image, with the correct domain and certificate. + # We need to also remove the 'mode=Padding' query because with it, it results in an error 400. + url = URL(self.data.display_pic) + if url.host == "images-eds.xboxlive.com": + url = url.with_host("images-eds-ssl.xboxlive.com") + query = dict(url.query) + query.pop("mode", None) + return str(url.with_query(query)) @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 109b2839b68..98e06257146 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -1,6 +1,7 @@ """Xbox friends binary sensors.""" +from __future__ import annotations + from functools import partial -from typing import Dict, List from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback @@ -44,7 +45,7 @@ class XboxBinarySensorEntity(XboxBaseSensorEntity, BinarySensorEntity): @callback def async_update_friends( coordinator: XboxUpdateCoordinator, - current: Dict[str, List[XboxBinarySensorEntity]], + current: dict[str, list[XboxBinarySensorEntity]], async_add_entities, ) -> None: """Update friends.""" @@ -73,7 +74,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: Dict[str, XboxBinarySensorEntity], + current: dict[str, XboxBinarySensorEntity], ) -> None: """Remove friend sensors from Home Assistant.""" registry = await async_get_entity_registry(coordinator.hass) diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index a91713931c2..0c3eec95c6f 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,5 +1,5 @@ """Support for media browsing.""" -from typing import Dict, List, Optional +from __future__ import annotations from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP @@ -41,7 +41,7 @@ async def build_item_response( tv_configured: bool, media_content_type: str, media_content_id: str, -) -> Optional[BrowseMedia]: +) -> BrowseMedia | None: """Create response payload for the provided media query.""" apps: InstalledPackagesList = await client.smartglass.get_installed_apps(device_id) @@ -149,7 +149,7 @@ async def build_item_response( ) -def item_payload(item: InstalledPackage, images: Dict[str, List[Image]]): +def item_payload(item: InstalledPackage, images: dict[str, list[Image]]): """Create response payload for a single media item.""" thumbnail = None image = _find_media_image(images.get(item.one_store_product_id, [])) @@ -169,7 +169,7 @@ def item_payload(item: InstalledPackage, images: Dict[str, List[Image]]): ) -def _find_media_image(images=List[Image]) -> Optional[Image]: +def _find_media_image(images: list[Image]) -> Image | None: purpose_order = ["Poster", "Tile", "Logo", "BoxArt"] for purpose in purpose_order: for image in images: diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 19e8a90d48e..e57f3971042 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -1,6 +1,8 @@ """Xbox Media Player Support.""" +from __future__ import annotations + import re -from typing import List, Optional +from typing import List from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.models import Image @@ -231,7 +233,7 @@ class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity): } -def _find_media_image(images=List[Image]) -> Optional[Image]: +def _find_media_image(images=List[Image]) -> Image | None: purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"] for purpose in purpose_order: for image in images: diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index 750300e49ee..64a16e2c21d 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -1,9 +1,10 @@ """Xbox Media Source Implementation.""" -from dataclasses import dataclass -from typing import List, Tuple +from __future__ import annotations -# pylint: disable=no-name-in-module -from pydantic.error_wrappers import ValidationError +from contextlib import suppress +from dataclasses import dataclass + +from pydantic.error_wrappers import ValidationError # pylint: disable=no-name-in-module from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.models import FieldsTemplate, Image from xbox.webapi.api.provider.gameclips.models import GameclipsResponse @@ -51,7 +52,7 @@ async def async_get_media_source(hass: HomeAssistantType): @callback def async_parse_identifier( item: MediaSourceItem, -) -> Tuple[str, str, str]: +) -> tuple[str, str, str]: """Parse identifier.""" identifier = item.identifier or "" start = ["", "", ""] @@ -88,7 +89,7 @@ class XboxSource(MediaSource): return PlayMedia(url, MIME_TYPE_MAP[kind]) async def async_browse_media( - self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES + self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES ) -> BrowseMediaSource: """Return media.""" title, category, _ = async_parse_identifier(item) @@ -137,8 +138,8 @@ class XboxSource(MediaSource): title_id, _, thumbnail = title.split("#", 2) owner, kind = category.split("#", 1) - items: List[XboxMediaItem] = [] - try: + items: list[XboxMediaItem] = [] + with suppress(ValidationError): # Unexpected API response if kind == "gameclips": if owner == "my": response: GameclipsResponse = ( @@ -189,9 +190,6 @@ class XboxSource(MediaSource): ) for item in response.screenshots ] - except ValidationError: - # Unexpected API response - pass return BrowseMediaSource( domain=DOMAIN, @@ -207,7 +205,7 @@ class XboxSource(MediaSource): ) -def _build_game_item(item: InstalledPackage, images: List[Image]): +def _build_game_item(item: InstalledPackage, images: list[Image]): """Build individual game.""" thumbnail = "" image = _find_media_image(images.get(item.one_store_product_id, [])) diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index e0ab661b2d9..ac19a4be193 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -1,7 +1,9 @@ """Xbox friends binary sensors.""" -from functools import partial -from typing import Dict, List +from __future__ import annotations +from functools import partial + +from homeassistant.components.sensor import SensorEntity from homeassistant.core import callback from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, @@ -28,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_ent update_friends() -class XboxSensorEntity(XboxBaseSensorEntity): +class XboxSensorEntity(XboxBaseSensorEntity, SensorEntity): """Representation of a Xbox presence state.""" @property @@ -43,7 +45,7 @@ class XboxSensorEntity(XboxBaseSensorEntity): @callback def async_update_friends( coordinator: XboxUpdateCoordinator, - current: Dict[str, List[XboxSensorEntity]], + current: dict[str, list[XboxSensorEntity]], async_add_entities, ) -> None: """Update friends.""" @@ -72,7 +74,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: Dict[str, XboxSensorEntity], + current: dict[str, XboxSensorEntity], ) -> None: """Remove friend sensors from Home Assistant.""" registry = await async_get_entity_registry(coordinator.hass) diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index 19f706be1c8..b35b1b8e2fc 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, "create_entry": { - "default": "Sikeres autentik\u00e1ci\u00f3" + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/id.json b/homeassistant/components/xbox/translations/id.json new file mode 100644 index 00000000000..ed8106b0144 --- /dev/null +++ b/homeassistant/components/xbox/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ko.json b/homeassistant/components/xbox/translations/ko.json index 7314d9e3c5c..0765928f8c9 100644 --- a/homeassistant/components/xbox/translations/ko.json +++ b/homeassistant/components/xbox/translations/ko.json @@ -3,7 +3,7 @@ "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.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index 300fcbfb095..2717bc1ad62 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -5,11 +5,10 @@ import logging import voluptuous as vol from xboxapi import Client -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -73,7 +72,7 @@ def get_user_gamercard(api, xuid): return None -class XboxSensor(Entity): +class XboxSensor(SensorEntity): """A class for the Xbox account.""" def __init__(self, api, xuid, gamercard, interval): @@ -104,7 +103,7 @@ class XboxSensor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attributes = {"gamerscore": self._gamerscore, "tier": self._tier} diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index f54c262abba..ba7f717f421 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -9,6 +9,7 @@ from xiaomi_gateway import XiaomiGateway, XiaomiGatewayDiscovery from homeassistant import config_entries, core from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_DEVICE_ID, ATTR_VOLTAGE, CONF_HOST, CONF_MAC, @@ -42,7 +43,6 @@ GATEWAY_PLATFORMS_NO_KEY = ["binary_sensor", "sensor"] ATTR_GW_MAC = "gw_mac" ATTR_RINGTONE_ID = "ringtone_id" ATTR_RINGTONE_VOL = "ringtone_vol" -ATTR_DEVICE_ID = "device_id" TIME_TILL_UNAVAILABLE = timedelta(minutes=150) @@ -188,9 +188,9 @@ async def async_setup_entry( else: platforms = GATEWAY_PLATFORMS_NO_KEY - for component in platforms: + for platform in platforms: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -208,8 +208,8 @@ async def async_unload_entry( unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in platforms + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in platforms ] ) ) @@ -241,7 +241,7 @@ class XiaomiDevice(Entity): self._type = device_type self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub - self._device_state_attributes = {} + self._extra_state_attributes = {} self._remove_unavailability_tracker = None self._xiaomi_hub = xiaomi_hub self.parse_data(device["data"], device["raw_data"]) @@ -319,9 +319,9 @@ class XiaomiDevice(Entity): return False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" - return self._device_state_attributes + return self._extra_state_attributes @callback def _async_set_unavailable(self, now): @@ -364,11 +364,11 @@ class XiaomiDevice(Entity): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] - self._device_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) + self._extra_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 - self._device_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1) + self._extra_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1) return True def parse_data(self, data, raw_data): diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 8fbecee46e9..3d9437e3778 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -170,10 +170,10 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_DENSITY: self._density} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs def parse_data(self, data, raw_data): @@ -214,10 +214,10 @@ class XiaomiMotionSensor(XiaomiBinarySensor): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_NO_MOTION_SINCE: self._no_motion_since} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs @callback @@ -308,10 +308,10 @@ class XiaomiDoorSensor(XiaomiBinarySensor): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_OPEN_SINCE: self._open_since} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs def parse_data(self, data, raw_data): @@ -389,10 +389,10 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): ) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_DENSITY: self._density} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs def parse_data(self, data, raw_data): @@ -424,10 +424,10 @@ class XiaomiVibration(XiaomiBinarySensor): super().__init__(device, name, xiaomi_hub, data_key, None, config_entry) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_LAST_ACTION: self._last_action} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs def parse_data(self, data, raw_data): @@ -459,10 +459,10 @@ class XiaomiButton(XiaomiBinarySensor): super().__init__(device, name, xiaomi_hub, data_key, None, config_entry) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_LAST_ACTION: self._last_action} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs def parse_data(self, data, raw_data): @@ -519,10 +519,10 @@ class XiaomiCube(XiaomiBinarySensor): super().__init__(device, "Cube", xiaomi_hub, data_key, None, config_entry) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attrs = {ATTR_LAST_ACTION: self._last_action} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs def parse_data(self, data, raw_data): diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 8028d16f86a..c080aec508d 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -10,7 +10,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_ from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac -# pylint: disable=unused-import from .const import ( CONF_INTERFACE, CONF_KEY, diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index 7c5334e0f5c..5afb1701e33 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -50,7 +50,7 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): return self._changed_by @property - def device_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict: """Return the state attributes.""" attributes = {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times} return attributes diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 5b1d3467d25..fa3d265f12f 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi Aqara sensors.""" import logging +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, @@ -107,7 +108,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class XiaomiSensor(XiaomiDevice): +class XiaomiSensor(XiaomiDevice, SensorEntity): """Representation of a XiaomiSensor.""" def __init__(self, device, name, data_key, xiaomi_hub, config_entry): @@ -171,7 +172,7 @@ class XiaomiSensor(XiaomiDevice): return True -class XiaomiBatterySensor(XiaomiDevice): +class XiaomiBatterySensor(XiaomiDevice, SensorEntity): """Representation of a XiaomiSensor.""" @property @@ -194,7 +195,7 @@ class XiaomiBatterySensor(XiaomiDevice): succeed = super().parse_voltage(data) if not succeed: return False - battery_level = int(self._device_state_attributes.pop(ATTR_BATTERY_LEVEL)) + battery_level = int(self._extra_state_attributes.pop(ATTR_BATTERY_LEVEL)) if battery_level <= 0 or battery_level > 100: return False self._state = battery_level diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 6e75ddb487e..8b16b6491c7 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -157,7 +157,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" if self._supports_power_consumption: attrs = { @@ -167,7 +167,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): } else: attrs = {} - attrs.update(super().device_state_attributes) + attrs.update(super().extra_state_attributes) return attrs @property diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index 6b0e25dfcd5..1effe228de6 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -14,7 +14,14 @@ "select": { "data": { "select_ip": "IP-Adresse" - } + }, + "description": "F\u00fchre das Setup erneut aus, wenn du zus\u00e4tzliche Gateways verbinden m\u00f6chtest" + }, + "settings": { + "data": { + "name": "Name des Gateways" + }, + "title": "Xiaomi Aqara Gateway, optionale Einstellungen" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index 1a69e20c6b1..295fcef83fe 100644 --- a/homeassistant/components/xiaomi_aqara/translations/hu.json +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut" + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "not_xiaomi_aqara": "Nem egy Xiaomi Aqara Gateway, a felfedezett eszk\u00f6z nem egyezett az ismert \u00e1tj\u00e1r\u00f3kkal" }, "error": { "discovery_error": "Nem siker\u00fclt felfedezni a Xiaomi Aqara K\u00f6zponti egys\u00e9get, pr\u00f3b\u00e1lja meg interf\u00e9szk\u00e9nt haszn\u00e1lni a HomeAssistant futtat\u00f3 eszk\u00f6z IP-j\u00e9t", - "invalid_host": " , l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm, l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u00c9rv\u00e9nytelen h\u00e1l\u00f3zati interf\u00e9sz", "invalid_key": "\u00c9rv\u00e9nytelen kulcs", "invalid_mac": "\u00c9rv\u00e9nytelen Mac-c\u00edm" @@ -17,7 +18,7 @@ "data": { "select_ip": "IP c\u00edm" }, - "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik K\u00f6zponti egys\u00e9get szeretne csatlakoztatni", + "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik k\u00f6zponti egys\u00e9get szeretne csatlakoztatni", "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get" }, "settings": { @@ -25,7 +26,7 @@ "key": "K\u00f6zponti egys\u00e9g kulcsa", "name": "K\u00f6zponti egys\u00e9g neve" }, - "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k f\u00e9rhetnek hozz\u00e1", + "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k lesznek hozz\u00e1f\u00e9rhet\u0151k", "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g, opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok" }, "user": { @@ -34,7 +35,7 @@ "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz", "mac": "Mac-c\u00edm (opcion\u00e1lis)" }, - "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a mac-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a MAC-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/id.json b/homeassistant/components/xiaomi_aqara/translations/id.json new file mode 100644 index 00000000000..5a2acfa330a --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/id.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "not_xiaomi_aqara": "Bukan Gateway Xiaomi Aqara, perangkat yang ditemukan tidak sesuai dengan gateway yang dikenal" + }, + "error": { + "discovery_error": "Gagal menemukan Xiaomi Aqara Gateway, coba gunakan IP perangkat yang menjalankan HomeAssistant sebagai antarmuka", + "invalid_host": "Nama host atau alamat IP tidak valid, lihat https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Antarmuka jaringan tidak valid", + "invalid_key": "Kunci gateway tidak valid", + "invalid_mac": "Alamat MAC Tidak Valid" + }, + "flow_title": "Xiaomi Aqara Gateway: {name}", + "step": { + "select": { + "data": { + "select_ip": "Alamat IP" + }, + "description": "Jalankan penyiapan lagi jika Anda ingin menghubungkan gateway lainnya", + "title": "Pilih Gateway Xiaomi Aqara yang ingin dihubungkan" + }, + "settings": { + "data": { + "key": "Kunci gateway Anda", + "name": "Nama Gateway" + }, + "description": "Kunci (kata sandi) dapat diambil menggunakan tutorial ini: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Jika kunci tidak disediakan, hanya sensor yang akan dapat diakses", + "title": "Xiaomi Aqara Gateway, pengaturan opsional" + }, + "user": { + "data": { + "host": "Alamat IP (opsional)", + "interface": "Antarmuka jaringan yang akan digunakan", + "mac": "Alamat MAC (opsional)" + }, + "description": "Hubungkan ke Xiaomi Aqara Gateway Anda, jika alamat IP dan MAC dibiarkan kosong, penemuan otomatis digunakan", + "title": "Xiaomi Aqara Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json index 7c15bc572e5..dd8a9ae5ede 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ko.json +++ b/homeassistant/components/xiaomi_aqara/translations/ko.json @@ -7,9 +7,10 @@ }, "error": { "discovery_error": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ubc1c\uacac\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. HomeAssistant \ub97c \uc778\ud130\ud398\uc774\uc2a4\ub85c \uc0ac\uc6a9\ud558\ub294 \uae30\uae30\uc758 IP \ub85c \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694.", - "invalid_host": "\ud638\uc2a4\ud2b8\uba85 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "invalid_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", "invalid_interface": "\ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "invalid_key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "invalid_key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_mac": "Mac \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "flow_title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774: {name}", "step": { @@ -25,14 +26,14 @@ "key": "\uac8c\uc774\ud2b8\uc6e8\uc774 \ud0a4", "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984" }, - "description": "\ud0a4(\ube44\ubc00\ubc88\ud638)\ub97c \uc5bb\uc740 \ubc29\ubc95\uc740 \ub2e4\uc74c\uc758 \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \ud0a4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc73c\uba74 \uc13c\uc11c\uc5d0\ub9cc \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\ud0a4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc73c\uba74 \uc13c\uc11c\uc5d0\ub9cc \uc811\uadfc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ud0a4(\ube44\ubc00\ubc88\ud638)\ub97c \uc5bb\ub294 \ubc29\ubc95\uc740 \ub2e4\uc74c\uc758 \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz", "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \ucd94\uac00 \uc124\uc815\ud558\uae30" }, "user": { "data": { "host": "IP \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)", "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4", - "mac": "Mac \uc8fc\uc18c(\uc120\ud0dd \uc0ac\ud56d)" + "mac": "Mac \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)" }, "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c \ubc0f MAC \uc8fc\uc18c\ub97c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774" diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index 81f984a5a05..a356ed36e1b 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -2,10 +2,14 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang" + "already_in_progress": "De configuratiestroom is al aan de gang", + "not_xiaomi_aqara": "Geen Xiaomi Aqara Gateway, ontdekt apparaat kwam niet overeen met bekende gateways" }, "error": { + "discovery_error": "Het is niet gelukt om een Xiaomi Aqara Gateway te vinden, probeer het IP van het apparaat waarop HomeAssistant draait als interface te gebruiken", "invalid_host": "Ongeldige hostnaam of IP-adres, zie https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Ongeldige netwerkinterface", + "invalid_key": "Ongeldige gatewaysleutel", "invalid_mac": "Ongeldig MAC-adres" }, "flow_title": "Xiaomi Aqara Gateway: {name}", @@ -18,11 +22,17 @@ "title": "Selecteer de Xiaomi Aqara Gateway waarmee u verbinding wilt maken" }, "settings": { - "description": "De sleutel (wachtwoord) kan worden opgehaald met behulp van deze tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Als de sleutel niet wordt meegeleverd, zijn alleen sensoren toegankelijk" + "data": { + "key": "De sleutel van uw gateway", + "name": "Naam van de Gateway" + }, + "description": "De sleutel (wachtwoord) kan worden opgehaald met behulp van deze tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Als de sleutel niet wordt meegeleverd, zijn alleen sensoren toegankelijk", + "title": "Xiaomi Aqara Gateway, optionele instellingen" }, "user": { "data": { "host": "IP-adres (optioneel)", + "interface": "De netwerkinterface die moet worden gebruikt", "mac": "MAC-adres (optioneel)" }, "description": "Maak verbinding met uw Xiaomi Aqara Gateway, als de IP- en mac-adressen leeg worden gelaten, wordt automatische detectie gebruikt", diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9c5f72d5877..f97d4623d69 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -2,20 +2,24 @@ from datetime import timedelta import logging -from miio.gateway import GatewayException +from miio.gateway.gateway import GatewayException from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( + ATTR_AVAILABLE, CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, CONF_MODEL, DOMAIN, KEY_COORDINATOR, + MODELS_AIR_MONITOR, + MODELS_FAN, + MODELS_LIGHT, MODELS_SWITCH, MODELS_VACUUM, ) @@ -23,9 +27,12 @@ from .gateway import ConnectXiaomiGateway _LOGGER = logging.getLogger(__name__) -GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "switch", "light"] +GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] +FAN_PLATFORMS = ["fan"] +LIGHT_PLATFORMS = ["light"] VACUUM_PLATFORMS = ["vacuum"] +AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] async def async_setup(hass: core.HomeAssistant, config: dict): @@ -38,14 +45,15 @@ async def async_setup_entry( ): """Set up the Xiaomi Miio components from a config entry.""" hass.data.setdefault(DOMAIN, {}) - if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: - if not await async_setup_gateway_entry(hass, entry): - return False - if entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - if not await async_setup_device_entry(hass, entry): - return False + if entry.data[ + CONF_FLOW_TYPE + ] == CONF_GATEWAY and not await async_setup_gateway_entry(hass, entry): + return False - return True + return bool( + entry.data[CONF_FLOW_TYPE] != CONF_DEVICE + or await async_setup_device_entry(hass, entry) + ) async def async_setup_gateway_entry( @@ -80,13 +88,22 @@ async def async_setup_gateway_entry( sw_version=gateway_info.firmware_version, ) - async def async_update_data(): + def update_data(): """Fetch data from the subdevice.""" - try: - for sub_device in gateway.gateway_device.devices.values(): - await hass.async_add_executor_job(sub_device.update) - except GatewayException as ex: - raise UpdateFailed("Got exception while fetching the state") from ex + data = {} + for sub_device in gateway.gateway_device.devices.values(): + try: + sub_device.update() + except GatewayException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + data[sub_device.sid] = {ATTR_AVAILABLE: False} + else: + data[sub_device.sid] = {ATTR_AVAILABLE: True} + return data + + async def async_update_data(): + """Fetch data from the subdevice using async_add_executor_job.""" + return await hass.async_add_executor_job(update_data) # Create update coordinator coordinator = DataUpdateCoordinator( @@ -104,9 +121,9 @@ async def async_setup_gateway_entry( KEY_COORDINATOR: coordinator, } - for component in GATEWAY_PLATFORMS: + for platform in GATEWAY_PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -122,16 +139,23 @@ async def async_setup_device_entry( platforms = [] if model in MODELS_SWITCH: platforms = SWITCH_PLATFORMS + elif model in MODELS_FAN: + platforms = FAN_PLATFORMS + elif model in MODELS_LIGHT: + platforms = LIGHT_PLATFORMS for vacuum_model in MODELS_VACUUM: if model.startswith(vacuum_model): platforms = VACUUM_PLATFORMS + for air_monitor_model in MODELS_AIR_MONITOR: + if model.startswith(air_monitor_model): + platforms = AIR_MONITOR_PLATFORMS if not platforms: return False - for component in platforms: + for platform in platforms: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 1e1e1b58632..4b56c60cd82 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,19 +1,25 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" import logging -from miio import AirQualityMonitor, Device, DeviceException +from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from homeassistant.exceptions import NoEntitySpecifiedError, PlatformNotReady import homeassistant.helpers.config_validation as cv from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MODEL, + DOMAIN, MODEL_AIRQUALITYMONITOR_B1, + MODEL_AIRQUALITYMONITOR_CGDN1, MODEL_AIRQUALITYMONITOR_S1, MODEL_AIRQUALITYMONITOR_V1, ) +from .device import XiaomiMiioEntity _LOGGER = logging.getLogger(__name__) @@ -40,53 +46,13 @@ PROP_TO_ATTR = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the sensor from config.""" - - host = config[CONF_HOST] - token = config[CONF_TOKEN] - name = config[CONF_NAME] - - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - - miio_device = Device(host, token) - - try: - device_info = await hass.async_add_executor_job(miio_device.info) - except DeviceException as ex: - raise PlatformNotReady from ex - - model = device_info.model - unique_id = f"{model}-{device_info.mac_address}" - _LOGGER.debug( - "%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version, - ) - - device = AirQualityMonitor(host, token, model=model) - - if model == MODEL_AIRQUALITYMONITOR_S1: - entity = AirMonitorS1(name, device, unique_id) - elif model == MODEL_AIRQUALITYMONITOR_B1: - entity = AirMonitorB1(name, device, unique_id) - elif model == MODEL_AIRQUALITYMONITOR_V1: - entity = AirMonitorV1(name, device, unique_id) - else: - raise NoEntitySpecifiedError(f"Not support for entity {unique_id}") - - async_add_entities([entity], update_before_add=True) - - -class AirMonitorB1(AirQualityEntity): +class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity): """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" - def __init__(self, name, device, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the entity.""" - self._name = name - self._device = device - self._unique_id = unique_id + super().__init__(name, device, entry, unique_id) + self._icon = "mdi:cloud" self._available = None self._air_quality_index = None @@ -112,11 +78,6 @@ class AirMonitorB1(AirQualityEntity): self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - @property def icon(self): """Return the icon to use for device if any.""" @@ -127,11 +88,6 @@ class AirMonitorB1(AirQualityEntity): """Return true when state is known.""" return self._available - @property - def unique_id(self): - """Return the unique ID.""" - return self._unique_id - @property def air_quality_index(self): """Return the Air Quality Index (AQI).""" @@ -168,7 +124,7 @@ class AirMonitorB1(AirQualityEntity): return self._humidity @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" data = {} @@ -219,3 +175,119 @@ class AirMonitorV1(AirMonitorB1): def unit_of_measurement(self): """Return the unit of measurement.""" return None + + +class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): + """Air Quality class for cgllc.airm.cgdn1 device.""" + + def __init__(self, name, device, entry, unique_id): + """Initialize the entity.""" + super().__init__(name, device, entry, unique_id) + + self._icon = "mdi:cloud" + self._available = None + self._carbon_dioxide = None + self._particulate_matter_2_5 = None + self._particulate_matter_10 = None + + async def async_update(self): + """Fetch state from the miio device.""" + try: + state = await self.hass.async_add_executor_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + self._carbon_dioxide = state.co2 + self._particulate_matter_2_5 = round(state.pm25, 1) + self._particulate_matter_10 = round(state.pm10, 1) + self._available = True + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return self._carbon_dioxide + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._particulate_matter_2_5 + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._particulate_matter_10 + + +DEVICE_MAP = { + MODEL_AIRQUALITYMONITOR_S1: { + "device_class": AirQualityMonitor, + "entity_class": AirMonitorS1, + }, + MODEL_AIRQUALITYMONITOR_B1: { + "device_class": AirQualityMonitor, + "entity_class": AirMonitorB1, + }, + MODEL_AIRQUALITYMONITOR_V1: { + "device_class": AirQualityMonitor, + "entity_class": AirMonitorV1, + }, + MODEL_AIRQUALITYMONITOR_CGDN1: { + "device_class": lambda host, token, model: AirQualityMonitorCGDN1(host, token), + "entity_class": AirMonitorCGDN1, + }, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Air Quality via platform setup is deprecated. " + "Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi Air Quality from a config entry.""" + entities = [] + + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + + if model in DEVICE_MAP: + device_entry = DEVICE_MAP[model] + entities.append( + device_entry["entity_class"]( + name, + device_entry["device_class"](host, token, model=model), + config_entry, + unique_id, + ) + ) + else: + _LOGGER.warning("AirQualityMonitor model '%s' is not yet supported", model) + + async_add_entities(entities, update_before_add=True) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index d6ee83e9842..9320972abcb 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -8,7 +8,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.helpers.device_registry import format_mac -# pylint: disable=unused-import from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, @@ -46,6 +45,8 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" + host = conf[CONF_HOST] + self.context.update({"title_placeholders": {"name": f"YAML import {host}"}}) return await self.async_step_device(user_input=conf) async def async_step_user(self, user_input=None): @@ -80,6 +81,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_device() + for device_model in MODELS_ALL_DEVICES: if name.startswith(device_model.replace(".", "-")): unique_id = self.mac diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index b2b63344c17..9b33bab08f7 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -9,6 +9,78 @@ CONF_MAC = "mac" KEY_COORDINATOR = "coordinator" +ATTR_AVAILABLE = "available" + +# Fan Models +MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1" +MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2" +MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3" +MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5" +MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6" +MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7" +MODEL_AIRPURIFIER_M1 = "zhimi.airpurifier.m1" +MODEL_AIRPURIFIER_M2 = "zhimi.airpurifier.m2" +MODEL_AIRPURIFIER_MA1 = "zhimi.airpurifier.ma1" +MODEL_AIRPURIFIER_MA2 = "zhimi.airpurifier.ma2" +MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1" +MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" +MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" +MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" +MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" +MODEL_AIRPURIFIER_PROH = "zhimi.airpurifier.va1" + +MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" +MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4" +MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1" + +MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" + +MODELS_PURIFIER_MIOT = [ + MODEL_AIRPURIFIER_3, + MODEL_AIRPURIFIER_3H, + MODEL_AIRPURIFIER_PROH, +] +MODELS_HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4] +MODELS_FAN_MIIO = [ + MODEL_AIRPURIFIER_V1, + MODEL_AIRPURIFIER_V2, + MODEL_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_V5, + MODEL_AIRPURIFIER_PRO, + MODEL_AIRPURIFIER_PRO_V7, + MODEL_AIRPURIFIER_M1, + MODEL_AIRPURIFIER_M2, + MODEL_AIRPURIFIER_MA1, + MODEL_AIRPURIFIER_MA2, + MODEL_AIRPURIFIER_SA1, + MODEL_AIRPURIFIER_SA2, + MODEL_AIRPURIFIER_2S, + MODEL_AIRHUMIDIFIER_V1, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRFRESH_VA2, +] + +# AirQuality Models +MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1" +MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1" +MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1" +MODEL_AIRQUALITYMONITOR_CGDN1 = "cgllc.airm.cgdn1" + +# Light Models +MODELS_LIGHT_EYECARE = ["philips.light.sread1"] +MODELS_LIGHT_CEILING = ["philips.light.ceiling", "philips.light.zyceiling"] +MODELS_LIGHT_MOON = ["philips.light.moonlight"] +MODELS_LIGHT_BULB = [ + "philips.light.bulb", + "philips.light.candle", + "philips.light.candle2", + "philips.light.downlight", +] +MODELS_LIGHT_MONO = ["philips.light.mono1"] + +# Model lists MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"] MODELS_SWITCH = [ "chuangmi.plug.v1", @@ -22,9 +94,25 @@ MODELS_SWITCH = [ "chuangmi.plug.hmi205", "chuangmi.plug.hmi206", ] +MODELS_FAN = MODELS_FAN_MIIO + MODELS_HUMIDIFIER_MIOT + MODELS_PURIFIER_MIOT +MODELS_LIGHT = ( + MODELS_LIGHT_EYECARE + + MODELS_LIGHT_CEILING + + MODELS_LIGHT_MOON + + MODELS_LIGHT_BULB + + MODELS_LIGHT_MONO +) MODELS_VACUUM = ["roborock.vacuum", "rockrobo.vacuum"] +MODELS_AIR_MONITOR = [ + MODEL_AIRQUALITYMONITOR_V1, + MODEL_AIRQUALITYMONITOR_B1, + MODEL_AIRQUALITYMONITOR_S1, + MODEL_AIRQUALITYMONITOR_CGDN1, +] -MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_VACUUM +MODELS_ALL_DEVICES = ( + MODELS_SWITCH + MODELS_VACUUM + MODELS_AIR_MONITOR + MODELS_FAN + MODELS_LIGHT +) MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY # Fan Services @@ -78,8 +166,3 @@ SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" SERVICE_CLEAN_SEGMENT = "vacuum_clean_segment" SERVICE_CLEAN_ZONE = "vacuum_clean_zone" SERVICE_GOTO = "vacuum_goto" - -# AirQuality Model -MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1" -MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1" -MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1" diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index ef527d0aa40..a6c1c7e5a28 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -1,7 +1,7 @@ """Support for Xiaomi Mi WiFi Repeater 2.""" import logging -from miio import DeviceException, WifiRepeater # pylint: disable=import-error +from miio import DeviceException, WifiRepeater import voluptuous as vol from homeassistant.components.device_tracker import ( diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 0d07654e61b..e20b1429bc6 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -4,33 +4,32 @@ from enum import Enum from functools import partial import logging -from miio import ( # pylint: disable=import-error +from miio import ( AirFresh, AirHumidifier, AirHumidifierMiot, AirPurifier, AirPurifierMiot, - Device, DeviceException, ) -from miio.airfresh import ( # pylint: disable=import-error, import-error +from miio.airfresh import ( LedBrightness as AirfreshLedBrightness, OperationMode as AirfreshOperationMode, ) -from miio.airhumidifier import ( # pylint: disable=import-error, import-error +from miio.airhumidifier import ( LedBrightness as AirhumidifierLedBrightness, OperationMode as AirhumidifierOperationMode, ) -from miio.airhumidifier_miot import ( # pylint: disable=import-error, import-error +from miio.airhumidifier_miot import ( LedBrightness as AirhumidifierMiotLedBrightness, OperationMode as AirhumidifierMiotOperationMode, PressedButton as AirhumidifierPressedButton, ) -from miio.airpurifier import ( # pylint: disable=import-error, import-error +from miio.airpurifier import ( LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, ) -from miio.airpurifier_miot import ( # pylint: disable=import-error, import-error +from miio.airpurifier_miot import ( LedBrightness as AirpurifierMiotLedBrightness, OperationMode as AirpurifierMiotOperationMode, ) @@ -44,18 +43,31 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, + ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, CONF_TOKEN, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, DOMAIN, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, + MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_PRO, + MODEL_AIRPURIFIER_PRO_V7, + MODEL_AIRPURIFIER_V3, + MODELS_FAN, + MODELS_HUMIDIFIER_MIOT, + MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_AUTO_DETECT_OFF, SERVICE_SET_AUTO_DETECT_ON, @@ -77,6 +89,7 @@ from .const import ( SERVICE_SET_TARGET_HUMIDITY, SERVICE_SET_VOLUME, ) +from .device import XiaomiMiioEntity _LOGGER = logging.getLogger(__name__) @@ -84,65 +97,20 @@ DEFAULT_NAME = "Xiaomi Miio Device" DATA_KEY = "fan.xiaomi_miio" CONF_MODEL = "model" -MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1" -MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2" -MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3" -MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5" -MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6" -MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7" -MODEL_AIRPURIFIER_M1 = "zhimi.airpurifier.m1" -MODEL_AIRPURIFIER_M2 = "zhimi.airpurifier.m2" -MODEL_AIRPURIFIER_MA1 = "zhimi.airpurifier.ma1" -MODEL_AIRPURIFIER_MA2 = "zhimi.airpurifier.ma2" -MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1" -MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" -MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" -MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" -MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" -MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" -MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" -MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4" -MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1" - -MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODEL): vol.In( - [ - MODEL_AIRPURIFIER_V1, - MODEL_AIRPURIFIER_V2, - MODEL_AIRPURIFIER_V3, - MODEL_AIRPURIFIER_V5, - MODEL_AIRPURIFIER_PRO, - MODEL_AIRPURIFIER_PRO_V7, - MODEL_AIRPURIFIER_M1, - MODEL_AIRPURIFIER_M2, - MODEL_AIRPURIFIER_MA1, - MODEL_AIRPURIFIER_MA2, - MODEL_AIRPURIFIER_SA1, - MODEL_AIRPURIFIER_SA2, - MODEL_AIRPURIFIER_2S, - MODEL_AIRPURIFIER_3, - MODEL_AIRPURIFIER_3H, - MODEL_AIRHUMIDIFIER_V1, - MODEL_AIRHUMIDIFIER_CA1, - MODEL_AIRHUMIDIFIER_CA4, - MODEL_AIRHUMIDIFIER_CB1, - MODEL_AIRFRESH_VA2, - ] - ), + vol.Optional(CONF_MODEL): vol.In(MODELS_FAN), } ) ATTR_MODEL = "model" # Air Purifier -ATTR_TEMPERATURE = "temperature" ATTR_HUMIDITY = "humidity" ATTR_AIR_QUALITY_INDEX = "aqi" ATTR_FILTER_HOURS_USED = "filter_hours_used" @@ -193,9 +161,6 @@ ATTR_FAULT = "fault" # Air Fresh ATTR_CO2 = "co2" -PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H] -HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4] - # Map attributes to properties of the state object AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { ATTR_TEMPERATURE: "temperature", @@ -553,104 +518,114 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the miio fan device from config.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Fan via platform setup is deprecated. " + "Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - host = config[CONF_HOST] - token = config[CONF_TOKEN] - name = config[CONF_NAME] - model = config.get(CONF_MODEL) - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - unique_id = None +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Fan from a config entry.""" + entities = [] - if model is None: - try: - miio_device = Device(host, token) - device_info = await hass.async_add_executor_job(miio_device.info) - model = device_info.model - unique_id = f"{model}-{device_info.mac_address}" - _LOGGER.info( - "%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version, + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + + if model in MODELS_PURIFIER_MIOT: + air_purifier = AirPurifierMiot(host, token) + entity = XiaomiAirPurifierMiot(name, air_purifier, config_entry, unique_id) + elif model.startswith("zhimi.airpurifier."): + air_purifier = AirPurifier(host, token) + entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id) + elif model in MODELS_HUMIDIFIER_MIOT: + air_humidifier = AirHumidifierMiot(host, token) + entity = XiaomiAirHumidifierMiot( + name, air_humidifier, config_entry, unique_id ) - except DeviceException as ex: - raise PlatformNotReady from ex - - if model in PURIFIER_MIOT: - air_purifier = AirPurifierMiot(host, token) - device = XiaomiAirPurifierMiot(name, air_purifier, model, unique_id) - elif model.startswith("zhimi.airpurifier."): - air_purifier = AirPurifier(host, token) - device = XiaomiAirPurifier(name, air_purifier, model, unique_id) - elif model in HUMIDIFIER_MIOT: - air_humidifier = AirHumidifierMiot(host, token) - device = XiaomiAirHumidifierMiot(name, air_humidifier, model, unique_id) - elif model.startswith("zhimi.humidifier."): - air_humidifier = AirHumidifier(host, token, model=model) - device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) - elif model.startswith("zhimi.airfresh."): - air_fresh = AirFresh(host, token) - device = XiaomiAirFresh(name, air_fresh, model, unique_id) - else: - _LOGGER.error( - "Unsupported device found! Please create an issue at " - "https://github.com/syssi/xiaomi_airpurifier/issues " - "and provide the following data: %s", - model, - ) - return False - - hass.data[DATA_KEY][host] = device - async_add_entities([device], update_before_add=True) - - async def async_service_handler(service): - """Map services to methods on XiaomiAirPurifier.""" - method = SERVICE_TO_METHOD.get(service.service) - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - devices = [ - device - for device in hass.data[DATA_KEY].values() - if device.entity_id in entity_ids - ] + elif model.startswith("zhimi.humidifier."): + air_humidifier = AirHumidifier(host, token, model=model) + entity = XiaomiAirHumidifier(name, air_humidifier, config_entry, unique_id) + elif model.startswith("zhimi.airfresh."): + air_fresh = AirFresh(host, token) + entity = XiaomiAirFresh(name, air_fresh, config_entry, unique_id) else: - devices = hass.data[DATA_KEY].values() + _LOGGER.error( + "Unsupported device found! Please create an issue at " + "https://github.com/syssi/xiaomi_airpurifier/issues " + "and provide the following data: %s", + model, + ) + return - update_tasks = [] - for device in devices: - if not hasattr(device, method["method"]): - continue - await getattr(device, method["method"])(**params) - update_tasks.append(device.async_update_ha_state(True)) + hass.data[DATA_KEY][host] = entity + entities.append(entity) - if update_tasks: - await asyncio.wait(update_tasks) + async def async_service_handler(service): + """Map services to methods on XiaomiAirPurifier.""" + method = SERVICE_TO_METHOD[service.service] + params = { + key: value + for key, value in service.data.items() + if key != ATTR_ENTITY_ID + } + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + entities = [ + entity + for entity in hass.data[DATA_KEY].values() + if entity.entity_id in entity_ids + ] + else: + entities = hass.data[DATA_KEY].values() - for air_purifier_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[air_purifier_service].get( - "schema", AIRPURIFIER_SERVICE_SCHEMA - ) - hass.services.async_register( - DOMAIN, air_purifier_service, async_service_handler, schema=schema - ) + update_tasks = [] + + for entity in entities: + entity_method = getattr(entity, method["method"], None) + if not entity_method: + continue + await entity_method(**params) + update_tasks.append( + hass.async_create_task(entity.async_update_ha_state(True)) + ) + + if update_tasks: + await asyncio.wait(update_tasks) + + for air_purifier_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[air_purifier_service].get( + "schema", AIRPURIFIER_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, air_purifier_service, async_service_handler, schema=schema + ) + + async_add_entities(entities, update_before_add=True) -class XiaomiGenericDevice(FanEntity): +class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" - def __init__(self, name, device, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the generic Xiaomi device.""" - self._name = name - self._device = device - self._model = model - self._unique_id = unique_id + super().__init__(name, device, entry, unique_id) self._available = False self._state = None @@ -668,23 +643,13 @@ class XiaomiGenericDevice(FanEntity): """Poll the device.""" return True - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - @property def available(self): """Return true when state is known.""" return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._state_attrs @@ -803,9 +768,9 @@ class XiaomiGenericDevice(FanEntity): class XiaomiAirPurifier(XiaomiGenericDevice): """Representation of a Xiaomi Air Purifier.""" - def __init__(self, name, device, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the plug switch.""" - super().__init__(name, device, model, unique_id) + super().__init__(name, device, entry, unique_id) if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -819,7 +784,7 @@ class XiaomiAirPurifier(XiaomiGenericDevice): self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S self._speed_list = OPERATION_MODES_AIRPURIFIER_2S - elif self._model == MODEL_AIRPURIFIER_3 or self._model == MODEL_AIRPURIFIER_3H: + elif self._model in MODELS_PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 self._speed_list = OPERATION_MODES_AIRPURIFIER_3 @@ -1056,9 +1021,9 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): class XiaomiAirHumidifier(XiaomiGenericDevice): """Representation of a Xiaomi Air Humidifier.""" - def __init__(self, name, device, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the plug switch.""" - super().__init__(name, device, model, unique_id) + super().__init__(name, device, entry, unique_id) if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB @@ -1214,7 +1179,6 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -1247,9 +1211,9 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" - def __init__(self, name, device, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the miio device.""" - super().__init__(name, device, model, unique_id) + super().__init__(name, device, entry, unique_id) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 356b19dc89a..be96f77240a 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -6,7 +6,7 @@ from miio import DeviceException, gateway from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import ATTR_AVAILABLE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -89,3 +89,11 @@ class XiaomiGatewayDevice(CoordinatorEntity, Entity): "model": self._sub_device.model, "sw_version": self._sub_device.firmware_version, } + + @property + def available(self): + """Return if entity is available.""" + if self.coordinator.data is None: + return False + + return self.coordinator.data[self._sub_device.sid][ATTR_AVAILABLE] diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 7f168cf0e3e..f6cd468ad00 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -7,8 +7,7 @@ import logging from math import ceil from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight -from miio import Device # pylint: disable=import-error -from miio.gateway import ( +from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, GATEWAY_MODEL_AC_V3, @@ -26,15 +25,24 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt from .const import ( + CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, + CONF_MODEL, DOMAIN, + KEY_COORDINATOR, + MODELS_LIGHT, + MODELS_LIGHT_BULB, + MODELS_LIGHT_CEILING, + MODELS_LIGHT_EYECARE, + MODELS_LIGHT_MONO, + MODELS_LIGHT_MOON, SERVICE_EYECARE_MODE_OFF, SERVICE_EYECARE_MODE_ON, SERVICE_NIGHT_LIGHT_MODE_OFF, @@ -44,32 +52,20 @@ from .const import ( SERVICE_SET_DELAYED_TURN_OFF, SERVICE_SET_SCENE, ) +from .device import XiaomiMiioEntity +from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Philips Light" DATA_KEY = "light.xiaomi_miio" -CONF_MODEL = "model" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODEL): vol.In( - [ - "philips.light.sread1", - "philips.light.ceiling", - "philips.light.zyceiling", - "philips.light.moonlight", - "philips.light.bulb", - "philips.light.candle", - "philips.light.candle2", - "philips.light.mono1", - "philips.light.downlight", - ] - ), + vol.Optional(CONF_MODEL): vol.In(MODELS_LIGHT), } ) @@ -81,7 +77,6 @@ DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4 DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1 SUCCESS = ["ok"] -ATTR_MODEL = "model" ATTR_SCENE = "scene" ATTR_DELAYED_TURN_OFF = "delayed_turn_off" ATTR_TIME_PERIOD = "time_period" @@ -94,8 +89,8 @@ ATTR_EYECARE_MODE = "eyecare_mode" ATTR_SLEEP_ASSISTANT = "sleep_assistant" ATTR_SLEEP_OFF_TIME = "sleep_off_time" ATTR_TOTAL_ASSISTANT_SLEEP_TIME = "total_assistant_sleep_time" -ATTR_BRAND_SLEEP = "brand_sleep" -ATTR_BRAND = "brand" +ATTR_BAND_SLEEP = "band_sleep" +ATTR_BAND = "band" XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) @@ -125,6 +120,21 @@ SERVICE_TO_METHOD = { } +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Light via platform setup is deprecated. " + "Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi light from a config entry.""" entities = [] @@ -140,148 +150,119 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append( XiaomiGatewayLight(gateway, config_entry.title, config_entry.unique_id) ) + # Gateway sub devices + sub_devices = gateway.devices + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + for sub_device in sub_devices.values(): + if sub_device.device_type == "LightBulb": + entities.append( + XiaomiGatewayBulb(coordinator, sub_device, config_entry) + ) + + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + model = config_entry.data[CONF_MODEL] + unique_id = config_entry.unique_id + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + + if model in MODELS_LIGHT_EYECARE: + light = PhilipsEyecare(host, token) + entity = XiaomiPhilipsEyecareLamp(name, light, config_entry, unique_id) + entities.append(entity) + hass.data[DATA_KEY][host] = entity + + entities.append( + XiaomiPhilipsEyecareLampAmbientLight( + name, light, config_entry, unique_id + ) + ) + # The ambient light doesn't expose additional services. + # A hass.data[DATA_KEY] entry isn't needed. + elif model in MODELS_LIGHT_CEILING: + light = Ceil(host, token) + entity = XiaomiPhilipsCeilingLamp(name, light, config_entry, unique_id) + entities.append(entity) + hass.data[DATA_KEY][host] = entity + elif model in MODELS_LIGHT_MOON: + light = PhilipsMoonlight(host, token) + entity = XiaomiPhilipsMoonlightLamp(name, light, config_entry, unique_id) + entities.append(entity) + hass.data[DATA_KEY][host] = entity + elif model in MODELS_LIGHT_BULB: + light = PhilipsBulb(host, token) + entity = XiaomiPhilipsBulb(name, light, config_entry, unique_id) + entities.append(entity) + hass.data[DATA_KEY][host] = entity + elif model in MODELS_LIGHT_MONO: + light = PhilipsBulb(host, token) + entity = XiaomiPhilipsGenericLight(name, light, config_entry, unique_id) + entities.append(entity) + hass.data[DATA_KEY][host] = entity + else: + _LOGGER.error( + "Unsupported device found! Please create an issue at " + "https://github.com/syssi/philipslight/issues " + "and provide the following data: %s", + model, + ) + return + + async def async_service_handler(service): + """Map services to methods on Xiaomi Philips Lights.""" + method = SERVICE_TO_METHOD.get(service.service) + params = { + key: value + for key, value in service.data.items() + if key != ATTR_ENTITY_ID + } + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + target_devices = [ + dev + for dev in hass.data[DATA_KEY].values() + if dev.entity_id in entity_ids + ] + else: + target_devices = hass.data[DATA_KEY].values() + + update_tasks = [] + for target_device in target_devices: + if not hasattr(target_device, method["method"]): + continue + await getattr(target_device, method["method"])(**params) + update_tasks.append(target_device.async_update_ha_state(True)) + + if update_tasks: + await asyncio.wait(update_tasks) + + for xiaomi_miio_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( + "schema", XIAOMI_MIIO_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema + ) async_add_entities(entities, update_before_add=True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the light from config.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config[CONF_HOST] - token = config[CONF_TOKEN] - name = config[CONF_NAME] - model = config.get(CONF_MODEL) - - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - - devices = [] - unique_id = None - - if model is None: - try: - miio_device = Device(host, token) - device_info = await hass.async_add_executor_job(miio_device.info) - model = device_info.model - unique_id = f"{model}-{device_info.mac_address}" - _LOGGER.info( - "%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version, - ) - except DeviceException as ex: - raise PlatformNotReady from ex - - if model == "philips.light.sread1": - light = PhilipsEyecare(host, token) - primary_device = XiaomiPhilipsEyecareLamp(name, light, model, unique_id) - devices.append(primary_device) - hass.data[DATA_KEY][host] = primary_device - - secondary_device = XiaomiPhilipsEyecareLampAmbientLight( - name, light, model, unique_id - ) - devices.append(secondary_device) - # The ambient light doesn't expose additional services. - # A hass.data[DATA_KEY] entry isn't needed. - elif model in ["philips.light.ceiling", "philips.light.zyceiling"]: - light = Ceil(host, token) - device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model == "philips.light.moonlight": - light = PhilipsMoonlight(host, token) - device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model in [ - "philips.light.bulb", - "philips.light.candle", - "philips.light.candle2", - "philips.light.downlight", - ]: - light = PhilipsBulb(host, token) - device = XiaomiPhilipsBulb(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - elif model == "philips.light.mono1": - light = PhilipsBulb(host, token) - device = XiaomiPhilipsGenericLight(name, light, model, unique_id) - devices.append(device) - hass.data[DATA_KEY][host] = device - else: - _LOGGER.error( - "Unsupported device found! Please create an issue at " - "https://github.com/syssi/philipslight/issues " - "and provide the following data: %s", - model, - ) - return False - - async_add_entities(devices, update_before_add=True) - - async def async_service_handler(service): - """Map services to methods on Xiaomi Philips Lights.""" - method = SERVICE_TO_METHOD.get(service.service) - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - target_devices = [ - dev - for dev in hass.data[DATA_KEY].values() - if dev.entity_id in entity_ids - ] - else: - target_devices = hass.data[DATA_KEY].values() - - update_tasks = [] - for target_device in target_devices: - if not hasattr(target_device, method["method"]): - continue - await getattr(target_device, method["method"])(**params) - update_tasks.append(target_device.async_update_ha_state(True)) - - if update_tasks: - await asyncio.wait(update_tasks) - - for xiaomi_miio_service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( - "schema", XIAOMI_MIIO_SERVICE_SCHEMA - ) - hass.services.async_register( - DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema - ) - - -class XiaomiPhilipsAbstractLight(LightEntity): +class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): """Representation of a Abstract Xiaomi Philips Light.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" - self._name = name - self._light = light - self._model = model - self._unique_id = unique_id + super().__init__(name, device, entry, unique_id) self._brightness = None - self._available = False self._state = None - self._state_attrs = {ATTR_MODEL: self._model} - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the device if any.""" - return self._name + self._state_attrs = {} @property def available(self): @@ -289,7 +270,7 @@ class XiaomiPhilipsAbstractLight(LightEntity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._state_attrs @@ -335,23 +316,23 @@ class XiaomiPhilipsAbstractLight(LightEntity): result = await self._try_command( "Setting brightness failed: %s", - self._light.set_brightness, + self._device.set_brightness, percent_brightness, ) if result: self._brightness = brightness else: - await self._try_command("Turning the light on failed.", self._light.on) + await self._try_command("Turning the light on failed.", self._device.on) async def async_turn_off(self, **kwargs): """Turn the light off.""" - await self._try_command("Turning the light off failed.", self._light.off) + await self._try_command("Turning the light off failed.", self._device.off) async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -368,16 +349,16 @@ class XiaomiPhilipsAbstractLight(LightEntity): class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): """Representation of a Generic Xiaomi Philips Light.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" - super().__init__(name, light, model, unique_id) + super().__init__(name, device, entry, unique_id) self._state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None}) async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -403,14 +384,14 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): async def async_set_scene(self, scene: int = 1): """Set the fixed scene.""" await self._try_command( - "Setting a fixed scene failed.", self._light.set_scene, scene + "Setting a fixed scene failed.", self._device.set_scene, scene ) async def async_set_delayed_turn_off(self, time_period: timedelta): """Set delayed turn off.""" await self._try_command( "Setting the turn off delay failed.", - self._light.delay_off, + self._device.delay_off, time_period.total_seconds(), ) @@ -439,9 +420,9 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Bulb.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" - super().__init__(name, light, model, unique_id) + super().__init__(name, device, entry, unique_id) self._color_temp = None @@ -489,7 +470,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): result = await self._try_command( "Setting brightness and color temperature failed: %s bri, %s cct", - self._light.set_brightness_and_color_temperature, + self._device.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, ) @@ -507,7 +488,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): result = await self._try_command( "Setting color temperature failed: %s cct", - self._light.set_color_temperature, + self._device.set_color_temperature, percent_color_temp, ) @@ -522,7 +503,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): result = await self._try_command( "Setting brightness failed: %s", - self._light.set_brightness, + self._device.set_brightness, percent_brightness, ) @@ -530,12 +511,12 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): self._brightness = brightness else: - await self._try_command("Turning the light on failed.", self._light.on) + await self._try_command("Turning the light on failed.", self._device.on) async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -573,9 +554,9 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Ceiling Lamp.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" - super().__init__(name, light, model, unique_id) + super().__init__(name, device, entry, unique_id) self._state_attrs.update( {ATTR_NIGHT_LIGHT_MODE: None, ATTR_AUTOMATIC_COLOR_TEMPERATURE: None} @@ -594,7 +575,7 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -629,9 +610,9 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Eyecare Lamp 2.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" - super().__init__(name, light, model, unique_id) + super().__init__(name, device, entry, unique_id) self._state_attrs.update( {ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None} @@ -640,7 +621,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -673,46 +654,46 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Set delayed turn off.""" await self._try_command( "Setting the turn off delay failed.", - self._light.delay_off, + self._device.delay_off, round(time_period.total_seconds() / 60), ) async def async_reminder_on(self): """Enable the eye fatigue notification.""" await self._try_command( - "Turning on the reminder failed.", self._light.reminder_on + "Turning on the reminder failed.", self._device.reminder_on ) async def async_reminder_off(self): """Disable the eye fatigue notification.""" await self._try_command( - "Turning off the reminder failed.", self._light.reminder_off + "Turning off the reminder failed.", self._device.reminder_off ) async def async_night_light_mode_on(self): """Turn the smart night light mode on.""" await self._try_command( "Turning on the smart night light mode failed.", - self._light.smart_night_light_on, + self._device.smart_night_light_on, ) async def async_night_light_mode_off(self): """Turn the smart night light mode off.""" await self._try_command( "Turning off the smart night light mode failed.", - self._light.smart_night_light_off, + self._device.smart_night_light_off, ) async def async_eyecare_mode_on(self): """Turn the eyecare mode on.""" await self._try_command( - "Turning on the eyecare mode failed.", self._light.eyecare_on + "Turning on the eyecare mode failed.", self._device.eyecare_on ) async def async_eyecare_mode_off(self): """Turn the eyecare mode off.""" await self._try_command( - "Turning off the eyecare mode failed.", self._light.eyecare_off + "Turning off the eyecare mode failed.", self._device.eyecare_off ) @staticmethod @@ -742,12 +723,12 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" name = f"{name} Ambient Light" if unique_id is not None: unique_id = f"{unique_id}-ambient" - super().__init__(name, light, model, unique_id) + super().__init__(name, device, entry, unique_id) async def async_turn_on(self, **kwargs): """Turn the light on.""" @@ -763,7 +744,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): result = await self._try_command( "Setting brightness of the ambient failed: %s", - self._light.set_ambient_brightness, + self._device.set_ambient_brightness, percent_brightness, ) @@ -771,19 +752,19 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): self._brightness = brightness else: await self._try_command( - "Turning the ambient light on failed.", self._light.ambient_on + "Turning the ambient light on failed.", self._device.ambient_on ) async def async_turn_off(self, **kwargs): """Turn the light off.""" await self._try_command( - "Turning the ambient light off failed.", self._light.ambient_off + "Turning the ambient light off failed.", self._device.ambient_off ) async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -800,9 +781,9 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Zhirui Bedside Lamp.""" - def __init__(self, name, light, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" - super().__init__(name, light, model, unique_id) + super().__init__(name, device, entry, unique_id) self._hs_color = None self._state_attrs.pop(ATTR_DELAYED_TURN_OFF) @@ -811,8 +792,8 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ATTR_SLEEP_ASSISTANT: None, ATTR_SLEEP_OFF_TIME: None, ATTR_TOTAL_ASSISTANT_SLEEP_TIME: None, - ATTR_BRAND_SLEEP: None, - ATTR_BRAND: None, + ATTR_BAND_SLEEP: None, + ATTR_BAND: None, } ) @@ -862,7 +843,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): result = await self._try_command( "Setting brightness and color failed: %s bri, %s color", - self._light.set_brightness_and_rgb, + self._device.set_brightness_and_rgb, percent_brightness, rgb, ) @@ -883,7 +864,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): result = await self._try_command( "Setting brightness and color temperature failed: %s bri, %s cct", - self._light.set_brightness_and_color_temperature, + self._device.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, ) @@ -896,7 +877,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): _LOGGER.debug("Setting color: %s", rgb) result = await self._try_command( - "Setting color failed: %s", self._light.set_rgb, rgb + "Setting color failed: %s", self._device.set_rgb, rgb ) if result: @@ -911,7 +892,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): result = await self._try_command( "Setting color temperature failed: %s cct", - self._light.set_color_temperature, + self._device.set_color_temperature, percent_color_temp, ) @@ -926,7 +907,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): result = await self._try_command( "Setting brightness failed: %s", - self._light.set_brightness, + self._device.set_brightness, percent_brightness, ) @@ -934,12 +915,12 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): self._brightness = brightness else: - await self._try_command("Turning the light on failed.", self._light.on) + await self._try_command("Turning the light on failed.", self._device.on) async def async_update(self): """Fetch state from the device.""" try: - state = await self.hass.async_add_executor_job(self._light.status) + state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: if self._available: self._available = False @@ -962,8 +943,8 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ATTR_SLEEP_ASSISTANT: state.sleep_assistant, ATTR_SLEEP_OFF_TIME: state.sleep_off_time, ATTR_TOTAL_ASSISTANT_SLEEP_TIME: state.total_assistant_sleep_time, - ATTR_BRAND_SLEEP: state.brand_sleep, - ATTR_BRAND: state.brand, + ATTR_BAND_SLEEP: state.brand_sleep, + ATTR_BAND: state.brand, } ) @@ -1071,3 +1052,57 @@ class XiaomiGatewayLight(LightEntity): self._brightness_pct = state_dict["brightness"] self._rgb = state_dict["rgb"] self._hs = color.color_RGB_to_hs(*self._rgb) + + +class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): + """Representation of Xiaomi Gateway Bulb.""" + + @property + def brightness(self): + """Return the brightness of the light.""" + return round((self._sub_device.status["brightness"] * 255) / 100) + + @property + def color_temp(self): + """Return current color temperature.""" + return self._sub_device.status["color_temp"] + + @property + def is_on(self): + """Return true if light is on.""" + return self._sub_device.status["status"] == "on" + + @property + def min_mireds(self): + """Return min cct.""" + return self._sub_device.status["cct_min"] + + @property + def max_mireds(self): + """Return max cct.""" + return self._sub_device.status["cct_max"] + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + async def async_turn_on(self, **kwargs): + """Instruct the light to turn on.""" + await self.hass.async_add_executor_job(self._sub_device.on) + + if ATTR_COLOR_TEMP in kwargs: + color_temp = kwargs[ATTR_COLOR_TEMP] + await self.hass.async_add_executor_job( + self._sub_device.set_color_temp, color_temp + ) + + if ATTR_BRIGHTNESS in kwargs: + brightness = round((kwargs[ATTR_BRIGHTNESS] * 100) / 255) + await self.hass.async_add_executor_job( + self._sub_device.set_brightness, brightness + ) + + async def async_turn_off(self, **kwargsf): + """Instruct the light to turn off.""" + await self.hass.async_add_executor_job(self._sub_device.off) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 2536b0e0aa7..6f8069be681 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "python-miio==0.5.4"], + "requirements": ["construct==2.10.56", "python-miio==0.5.5"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."] } diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index c946533ab54..7d75e943d4d 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging import time -from miio import ChuangmiIr, DeviceException # pylint: disable=import-error +from miio import ChuangmiIr, DeviceException import voluptuous as vol from homeassistant.components.remote import ( diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index a47b32fc6d1..ac9a7ab4543 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -2,9 +2,8 @@ from dataclasses import dataclass import logging -from miio import AirQualityMonitor # pylint: disable=import-error -from miio import DeviceException -from miio.gateway import ( +from miio import AirQualityMonitor, DeviceException +from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, GATEWAY_MODEL_AC_V3, @@ -13,30 +12,32 @@ from miio.gateway import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( + ATTR_BATTERY_LEVEL, CONF_HOST, CONF_NAME, CONF_TOKEN, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, + POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from .const import CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, KEY_COORDINATOR +from .const import CONF_DEVICE, CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, KEY_COORDINATOR +from .device import XiaomiMiioEntity from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Sensor" -DATA_KEY = "sensor.xiaomi_miio" UNIT_LUMEN = "lm" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -49,13 +50,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ATTR_POWER = "power" ATTR_CHARGING = "charging" -ATTR_BATTERY_LEVEL = "battery_level" ATTR_DISPLAY_CLOCK = "display_clock" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" ATTR_SENSOR_STATE = "sensor_state" -ATTR_MODEL = "model" SUCCESS = ["ok"] @@ -79,9 +78,27 @@ GATEWAY_SENSOR_TYPES = { "pressure": SensorType( unit=PRESSURE_HPA, icon=None, device_class=DEVICE_CLASS_PRESSURE ), + "load_power": SensorType( + unit=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER + ), } +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Sensor via platform setup is deprecated. " + "Please remove it from your configuration" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi sensor from a config entry.""" entities = [] @@ -115,48 +132,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] ) + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + unique_id = config_entry.unique_id + + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + + device = AirQualityMonitor(host, token) + entities.append(XiaomiAirQualityMonitor(name, device, config_entry, unique_id)) + async_add_entities(entities, update_before_add=True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the sensor from config.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config[CONF_HOST] - token = config[CONF_TOKEN] - name = config[CONF_NAME] - - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - - try: - air_quality_monitor = AirQualityMonitor(host, token) - device_info = await hass.async_add_executor_job(air_quality_monitor.info) - model = device_info.model - unique_id = f"{model}-{device_info.mac_address}" - _LOGGER.info( - "%s %s %s detected", - model, - device_info.firmware_version, - device_info.hardware_version, - ) - device = XiaomiAirQualityMonitor(name, air_quality_monitor, model, unique_id) - except DeviceException as ex: - raise PlatformNotReady from ex - - hass.data[DATA_KEY][host] = device - async_add_entities([device], update_before_add=True) - - -class XiaomiAirQualityMonitor(Entity): +class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Representation of a Xiaomi Air Quality Monitor.""" - def __init__(self, name, device, model, unique_id): + def __init__(self, name, device, entry, unique_id): """Initialize the entity.""" - self._name = name - self._device = device - self._model = model - self._unique_id = unique_id + super().__init__(name, device, entry, unique_id) self._icon = "mdi:cloud" self._unit_of_measurement = "AQI" @@ -171,19 +166,8 @@ class XiaomiAirQualityMonitor(Entity): ATTR_NIGHT_TIME_BEGIN: None, ATTR_NIGHT_TIME_END: None, ATTR_SENSOR_STATE: None, - ATTR_MODEL: self._model, } - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -205,7 +189,7 @@ class XiaomiAirQualityMonitor(Entity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._state_attrs @@ -236,7 +220,7 @@ class XiaomiAirQualityMonitor(Entity): _LOGGER.error("Got exception while fetching the state: %s", ex) -class XiaomiGatewaySensor(XiaomiGatewayDevice): +class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): """Representation of a XiaomiGatewaySensor.""" def __init__(self, coordinator, sub_device, entry, data_key): @@ -267,7 +251,7 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice): return self._sub_device.status[self._data_key] -class XiaomiGatewayIlluminanceSensor(Entity): +class XiaomiGatewayIlluminanceSensor(SensorEntity): """Representation of the gateway device's illuminance sensor.""" def __init__(self, gateway_device, gateway_name, gateway_device_id): diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index e523dfb0bb7..09a9786c372 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -3,16 +3,20 @@ import asyncio from functools import partial import logging -from miio import AirConditioningCompanionV3 # pylint: disable=import-error -from miio import ChuangmiPlug, DeviceException, PowerStrip -from miio.powerstrip import PowerMode # pylint: disable=import-error +from miio import AirConditioningCompanionV3, ChuangmiPlug, DeviceException, PowerStrip +from miio.powerstrip import PowerMode import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DEVICE_CLASS_SWITCH, + PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, + ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, CONF_TOKEN, @@ -25,12 +29,14 @@ from .const import ( CONF_GATEWAY, CONF_MODEL, DOMAIN, + KEY_COORDINATOR, SERVICE_SET_POWER_MODE, SERVICE_SET_POWER_PRICE, SERVICE_SET_WIFI_LED_OFF, SERVICE_SET_WIFI_LED_ON, ) from .device import XiaomiMiioEntity +from .gateway import XiaomiGatewayDevice _LOGGER = logging.getLogger(__name__) @@ -40,6 +46,13 @@ DATA_KEY = "switch.xiaomi_miio" MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2" MODEL_PLUG_V3 = "chuangmi.plug.v3" +KEY_CHANNEL = "channel" +GATEWAY_SWITCH_VARS = { + "status_ch0": {KEY_CHANNEL: 0}, + "status_ch1": {KEY_CHANNEL: 1}, + "status_ch2": {KEY_CHANNEL: 2}, +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -64,7 +77,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) ATTR_POWER = "power" -ATTR_TEMPERATURE = "temperature" ATTR_LOAD_POWER = "load_power" ATTR_MODEL = "model" ATTR_POWER_MODE = "power_mode" @@ -115,7 +127,7 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Import Miio configuration from YAML.""" _LOGGER.warning( - "Loading Xiaomi Miio Switch via platform setup is deprecated. Please remove it from your configuration." + "Loading Xiaomi Miio Switch via platform setup is deprecated; Please remove it from your configuration" ) hass.async_create_task( hass.config_entries.flow.async_init( @@ -136,6 +148,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id + if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: + gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] + # Gateway sub devices + sub_devices = gateway.devices + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + for sub_device in sub_devices.values(): + if sub_device.device_type != "Switch": + continue + switch_variables = set(sub_device.status) & set(GATEWAY_SWITCH_VARS) + if switch_variables: + entities.extend( + [ + XiaomiGatewaySwitch( + coordinator, sub_device, config_entry, variable + ) + for variable in switch_variables + ] + ) + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE or ( config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY and model == "lumi.acpartner.v3" @@ -228,6 +259,40 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, update_before_add=True) +class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): + """Representation of a XiaomiGatewaySwitch.""" + + def __init__(self, coordinator, sub_device, entry, variable): + """Initialize the XiaomiSensor.""" + super().__init__(coordinator, sub_device, entry) + self._channel = GATEWAY_SWITCH_VARS[variable][KEY_CHANNEL] + self._data_key = f"status_ch{self._channel}" + self._unique_id = f"{sub_device.sid}-ch{self._channel}" + self._name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})" + + @property + def device_class(self): + """Return the device class of this entity.""" + return DEVICE_CLASS_SWITCH + + @property + def is_on(self): + """Return true if switch is on.""" + return self._sub_device.status[self._data_key] == "on" + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self.hass.async_add_executor_job(self._sub_device.on, self._channel) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self.hass.async_add_executor_job(self._sub_device.off, self._channel) + + async def async_toggle(self, **kwargs): + """Toggle the switch.""" + await self.hass.async_add_executor_job(self._sub_device.toggle, self._channel) + + class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" @@ -253,7 +318,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): return self._available @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._state_attrs diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json new file mode 100644 index 00000000000..bd5387fe8f9 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "device": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index 170d14fc6dc..f8dee0efe69 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -18,7 +18,7 @@ "name": "Nom del dispositiu", "token": "Token d'API" }, - "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", + "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la de Xiaomi" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 7cf11a1085e..2817d18b578 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -6,16 +6,20 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus." + "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", + "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." }, "flow_title": "Xiaomi Miio: {name}", "step": { "device": { "data": { "host": "IP-Adresse", + "model": "Ger\u00e4temodell (optional)", "name": "Name des Ger\u00e4ts", "token": "API-Token" - } + }, + "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr eine Anleitung. Dieser unterscheidet sich vom API-Token, den die Xiaomi Aqara-Integration nutzt.", + "title": "Herstellen einer Verbindung mit einem Xiaomi Miio-Ger\u00e4t oder Xiaomi Gateway" }, "gateway": { "data": { @@ -23,14 +27,14 @@ "name": "Name des Gateways", "token": "API-Token" }, - "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, "user": { "data": { "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, - "description": "W\u00e4hle aus, mit welchem Ger\u00e4t du eine Verbindung herstellen m\u00f6chtest.", + "description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index beb5c06c098..e5cf4501608 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut" + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -10,20 +10,30 @@ }, "flow_title": "Xiaomi Miio: {name}", "step": { + "device": { + "data": { + "host": "IP c\u00edm", + "model": "Eszk\u00f6z modell (opcion\u00e1lis)", + "name": "Eszk\u00f6z neve", + "token": "API Token" + }, + "description": "Sz\u00fcks\u00e9ged lesz a 32 karakteres API Tokenre, k\u00f6vesd a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token oldal instrukci\u00f3it. Vedd figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", + "title": "Csatlakoz\u00e1s Xiaomi Miio eszk\u00f6zh\u00f6z vagy Xiaomi Gateway-hez" + }, "gateway": { "data": { "host": "IP c\u00edm", "name": "K\u00f6zponti egys\u00e9g neve", "token": "API Token" }, - "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token", + "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. K\u00e9rj\u00fck, vegye figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", "title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" }, "user": { "data": { "gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" }, - "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. ", + "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni.", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json new file mode 100644 index 00000000000..d55e19980a7 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "no_device_selected": "Tidak ada perangkat yang dipilih, pilih satu perangkat.", + "unknown_device": "Model perangkat tidak diketahui, tidak dapat menyiapkan perangkat menggunakan alur konfigurasi." + }, + "flow_title": "Xiaomi Miio: {name}", + "step": { + "device": { + "data": { + "host": "Alamat IP", + "model": "Model perangkat (Opsional)", + "name": "Nama perangkat", + "token": "Token API" + }, + "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.", + "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + }, + "gateway": { + "data": { + "host": "Alamat IP", + "name": "Nama Gateway", + "token": "Token API" + }, + "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.", + "title": "Hubungkan ke Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Hubungkan ke Xiaomi Gateway" + }, + "description": "Pilih perangkat mana yang ingin disambungkan.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index aa48ba7cfa8..7eec7d7e424 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -18,7 +18,7 @@ "name": "Nome del dispositivo", "token": "Token API" }, - "description": "Avrai bisogno dei 32 caratteri Token API , vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per istruzioni. Tieni presente che questa Token API \u00e8 diversa dalla chiave utilizzata dall'integrazione Xiaomi Aqara.", + "description": "Avrai bisogno dei 32 caratteri della Token API, vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per istruzioni. Tieni presente che questa Token API \u00e8 diversa dalla chiave utilizzata dall'integrazione Xiaomi Aqara.", "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/ko.json b/homeassistant/components/xiaomi_miio/translations/ko.json index 7e594fde247..03043f92957 100644 --- a/homeassistant/components/xiaomi_miio/translations/ko.json +++ b/homeassistant/components/xiaomi_miio/translations/ko.json @@ -6,15 +6,20 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "unknown_device": "\uae30\uae30\uc758 \ubaa8\ub378\uc744 \uc54c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ud750\ub984\uc5d0\uc11c \uae30\uae30\ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "flow_title": "Xiaomi Miio: {name}", "step": { "device": { "data": { "host": "IP \uc8fc\uc18c", + "model": "\uae30\uae30 \ubaa8\ub378 (\uc120\ud0dd \uc0ac\ud56d)", + "name": "\uae30\uae30 \uc774\ub984", "token": "API \ud1a0\ud070" - } + }, + "description": "32\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", + "title": "Xiaomi Miio \uae30\uae30 \ub610\ub294 Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" }, "gateway": { "data": { @@ -22,7 +27,7 @@ "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984", "token": "API \ud1a0\ud070" }, - "description": "32 \uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", + "description": "32\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", "title": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 66209e61ee6..394d43fc261 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -2,11 +2,12 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom voor dit Xiaomi Miio-apparaat is al bezig." + "already_in_progress": "De configuratiestroom is al aan de gang" }, "error": { "cannot_connect": "Kan geen verbinding maken", - "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft" + "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft", + "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow." }, "flow_title": "Xiaomi Miio: {name}", "step": { @@ -16,7 +17,9 @@ "model": "Apparaatmodel (Optioneel)", "name": "Naam van het apparaat", "token": "API-token" - } + }, + "description": "U hebt de 32 karakter API-token nodig, zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Let op, deze API-token is anders dan de sleutel die wordt gebruikt door de Xiaomi Aqara integratie.", + "title": "Verbinding maken met een Xiaomi Miio-apparaat of Xiaomi Gateway" }, "gateway": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 0a6cf433d87..74a398a9ba6 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -18,7 +18,7 @@ "name": "Navnet p\u00e5 enheten", "token": "API-token" }, - "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", + "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 80528b71370..8b7105b6736 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -18,7 +18,7 @@ "name": "Nazwa urz\u0105dzenia", "token": "Token API" }, - "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.", + "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.", "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi b\u0105d\u017a innym urz\u0105dzeniem Xiaomi Miio" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index 65edf2dbe31..922c2441c3d 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -7,6 +7,12 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "device": { + "data": { + "host": "Endere\u00e7o IP", + "token": "API Token" + } + }, "gateway": { "data": { "host": "Endere\u00e7o IP", diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index 5c5064ac347..b17291746b3 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -18,7 +18,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "token": "\u0422\u043e\u043a\u0435\u043d API" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", + "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 3b0a89b7485..db1d825cea8 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -18,7 +18,7 @@ "name": "\u88dd\u7f6e\u540d\u7a31", "token": "API \u6b0a\u6756" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 7bdbfca7bc9..8551a80ff89 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -2,7 +2,7 @@ from functools import partial import logging -from miio import DeviceException, Vacuum # pylint: disable=import-error +from miio import DeviceException, Vacuum import voluptuous as vol from homeassistant.components.vacuum import ( @@ -122,7 +122,7 @@ STATE_CODE_TO_STATE = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Import Miio configuration from YAML.""" _LOGGER.warning( - "Loading Xiaomi Miio Vacuum via platform setup is deprecated. Please remove it from your configuration." + "Loading Xiaomi Miio Vacuum via platform setup is deprecated; Please remove it from your configuration" ) hass.async_create_task( hass.config_entries.flow.async_init( @@ -305,7 +305,7 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): ] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the specific state attributes of this vacuum cleaner.""" attrs = {} if self.vacuum_state is not None: diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 68a041a2887..2abd3ffa245 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -165,7 +165,7 @@ async def async_send_message( if message: self.send_text_message() - self.disconnect(wait=True) + self.disconnect() async def send_file(self, timeout=None): """Send file via XMPP. @@ -174,7 +174,7 @@ async def async_send_message( HTTP Upload (XEP_0363) """ if room: - self.plugin["xep_0045"].join_muc(room, sender, wait=True) + self.plugin["xep_0045"].join_muc(room, sender) try: # Uploading with XEP_0363 @@ -335,7 +335,7 @@ async def async_send_message( try: if room: _LOGGER.debug("Joining room %s", room) - self.plugin["xep_0045"].join_muc(room, sender, wait=True) + self.plugin["xep_0045"].join_muc(room, sender) self.send_message(mto=room, mbody=message, mtype="groupchat") else: for recipient in recipients: diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py index 8651c33546c..1d65b2bcfd1 100644 --- a/homeassistant/components/xs1/__init__.py +++ b/homeassistant/components/xs1/__init__.py @@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -XS1_COMPONENTS = ["climate", "sensor", "switch"] +PLATFORMS = ["climate", "sensor", "switch"] # Lock used to limit the amount of concurrent update requests # as the XS1 Gateway can only handle a very @@ -47,7 +47,7 @@ UPDATE_LOCK = asyncio.Lock() def setup(hass, config): - """Set up XS1 Component.""" + """Set up XS1 integration.""" _LOGGER.debug("Initializing XS1") host = config[DOMAIN][CONF_HOST] @@ -68,7 +68,7 @@ def setup(hass, config): ) return False - _LOGGER.debug("Establishing connection to XS1 gateway and retrieving data...") + _LOGGER.debug("Establishing connection to XS1 gateway and retrieving data") hass.data[DOMAIN] = {} @@ -78,10 +78,10 @@ def setup(hass, config): hass.data[DOMAIN][ACTUATORS] = actuators hass.data[DOMAIN][SENSORS] = sensors - _LOGGER.debug("Loading components for XS1 platform...") - # Load components for supported devices - for component in XS1_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + _LOGGER.debug("Loading platforms for XS1 integration") + # Load platforms for supported devices + for platform in PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py index 9e6afa40fa4..f158e7d74b8 100644 --- a/homeassistant/components/xs1/sensor.py +++ b/homeassistant/components/xs1/sensor.py @@ -1,7 +1,7 @@ """Support for XS1 sensors.""" from xs1_api_client.api_constants import ActuatorType -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from . import ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensor_entities) -class XS1Sensor(XS1DeviceEntity, Entity): +class XS1Sensor(XS1DeviceEntity, SensorEntity): """Representation of a Sensor.""" @property diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 957844e519d..08e856a721e 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -6,11 +6,10 @@ import logging from aioymaps import YandexMapsRequester import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -47,7 +46,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([DiscoverYandexTransport(data, stop_id, routes, name)], True) -class DiscoverYandexTransport(Entity): +class DiscoverYandexTransport(SensorEntity): """Implementation of yandex_transport sensor.""" def __init__(self, requester: YandexMapsRequester, stop_id, routes, name): @@ -124,7 +123,7 @@ class DiscoverYandexTransport(Entity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index f24847a2d54..c1e0c555e02 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,8 +1,9 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import Optional import voluptuous as vol from yeelight import Bulb, BulbException, discover_bulbs @@ -181,7 +182,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" - async def _initialize(host: str, capabilities: Optional[dict] = None) -> None: + async def _initialize(host: str, capabilities: dict | None = None) -> None: remove_dispatcher = async_dispatcher_connect( hass, DEVICE_INITIALIZED.format(host), @@ -198,9 +199,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _load_platforms(): - for component in PLATFORMS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) # Move options from data for imported entries @@ -246,8 +247,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) @@ -593,7 +594,7 @@ async def _async_get_device( hass: HomeAssistant, host: str, entry: ConfigEntry, - capabilities: Optional[dict], + capabilities: dict | None, ) -> YeelightDevice: # Get model from config and capabilities model = entry.options.get(CONF_MODEL) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index f186c897a21..0473cc1042c 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -16,10 +16,10 @@ from . import ( CONF_NIGHTLIGHT_SWITCH_TYPE, CONF_SAVE_ON_CHANGE, CONF_TRANSITION, + DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, _async_unique_name, ) -from . import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index e4044303ef0..218bcbbdb27 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,10 +1,13 @@ """Light platform support for yeelight.""" +from __future__ import annotations + from functools import partial import logging import voluptuous as vol import yeelight from yeelight import ( + Bulb, BulbException, Flow, RGBTransition, @@ -529,9 +532,8 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return the current effect.""" return self._effect - # F821: https://github.com/PyCQA/pyflakes/issues/373 @property - def _bulb(self) -> "Bulb": # noqa: F821 + def _bulb(self) -> Bulb: return self.device.bulb @property @@ -560,7 +562,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return YEELIGHT_MONO_EFFECT_LIST @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attributes = { "flowing": self.device.is_color_flow_enabled, diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json index 10a03cebd21..ac463142359 100644 --- a/homeassistant/components/yeelight/translations/hu.json +++ b/homeassistant/components/yeelight/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton" + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Gazdag\u00e9p" + "host": "Hoszt" }, "description": "Ha a gazdag\u00e9pet \u00fcresen hagyja, felder\u00edt\u00e9sre ker\u00fcl automatikusan." } diff --git a/homeassistant/components/yeelight/translations/id.json b/homeassistant/components/yeelight/translations/id.json new file mode 100644 index 00000000000..0c81739095d --- /dev/null +++ b/homeassistant/components/yeelight/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "pick_device": { + "data": { + "device": "Perangkat" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (Opsional)", + "nightlight_switch": "Gunakan Sakelar Lampu Malam", + "save_on_change": "Simpan Status Saat Berubah", + "transition": "Waktu Transisi (milidetik)", + "use_music_mode": "Aktifkan Mode Musik" + }, + "description": "Jika model dibiarkan kosong, model akan dideteksi secara otomatis." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json index 1d6974aaa61..4abb8fcbbff 100644 --- a/homeassistant/components/yeelight/translations/ko.json +++ b/homeassistant/components/yeelight/translations/ko.json @@ -10,14 +10,14 @@ "step": { "pick_device": { "data": { - "device": "\uc7a5\uce58" + "device": "\uae30\uae30" } }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "\ud638\uc2a4\ud2b8\ub97c \ube44\uc6cc\ub450\uba74 \uc7a5\uce58\ub97c \ucc3e\ub294 \ub370 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4." + "description": "\ud638\uc2a4\ud2b8\ub97c \ube44\uc6cc \ub450\uba74 \uae30\uae30\ub97c \ucc3e\ub294 \ub370 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4" } } }, @@ -25,11 +25,11 @@ "step": { "init": { "data": { - "model": "\ubaa8\ub378(\uc120\ud0dd \uc0ac\ud56d)", - "nightlight_switch": "\uc57c\uac04 \uc870\uba85 \uc2a4\uc704\uce58 \uc0ac\uc6a9", - "save_on_change": "\ubcc0\uacbd\uc2dc \uc0c1\ud0dc \uc800\uc7a5", + "model": "\ubaa8\ub378 (\uc120\ud0dd \uc0ac\ud56d)", + "nightlight_switch": "\uc57c\uac04 \uc870\uba85 \uc804\ud658 \uc0ac\uc6a9\ud558\uae30", + "save_on_change": "\ubcc0\uacbd \uc2dc \uc0c1\ud0dc\ub97c \uc800\uc7a5\ud558\uae30", "transition": "\uc804\ud658 \uc2dc\uac04(ms)", - "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654" + "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654\ud558\uae30" }, "description": "\ubaa8\ub378\uc744 \ube44\uc6cc \ub450\uba74 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub429\ub2c8\ub2e4." } diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index b9d02731570..82807c4acee 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -1,4 +1,5 @@ """Support for Zabbix.""" +from contextlib import suppress import json import logging import math @@ -202,7 +203,7 @@ class ZabbixThread(threading.Thread): dropped = 0 - try: + with suppress(queue.Empty): while len(metrics) < BATCH_BUFFER_SIZE and not self.shutdown: timeout = None if count == 0 else BATCH_TIMEOUT item = self.queue.get(timeout=timeout) @@ -223,9 +224,6 @@ class ZabbixThread(threading.Thread): else: dropped += 1 - except queue.Empty: - pass - if dropped: _LOGGER.warning("Catching up, dropped %d old events", dropped) diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 3fa29a07896..a2644287690 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -4,10 +4,9 @@ import logging import voluptuous as vol from homeassistant.components import zabbix -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -37,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zapi = hass.data[zabbix.DOMAIN] if not zapi: - _LOGGER.error("zapi is None. Zabbix integration hasn't been loaded?") + _LOGGER.error("Zabbix integration hasn't been loaded? zapi is None") return False _LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version()) @@ -79,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class ZabbixTriggerCountSensor(Entity): +class ZabbixTriggerCountSensor(SensorEntity): """Get the active trigger count for all Zabbix monitored hosts.""" def __init__(self, zApi, name="Zabbix"): @@ -116,7 +115,7 @@ class ZabbixTriggerCountSensor(Entity): self._state = len(triggers) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes of the device.""" return self._attributes diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 4852e874672..2e2d07cea62 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -11,6 +11,7 @@ import pytz import requests import voluptuous as vol +from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_ATTRIBUTION, @@ -27,7 +28,6 @@ from homeassistant.const import ( __version__, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -132,7 +132,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ZamgSensor(Entity): +class ZamgSensor(SensorEntity): """Implementation of a ZAMG sensor.""" def __init__(self, probe, variable, name): @@ -157,7 +157,7 @@ class ZamgSensor(Entity): return SENSOR_TYPES[self.variable][1] @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2ef7db3a1b4..38544798b9b 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,19 +1,20 @@ """Support for exposing Home Assistant via Zeroconf.""" +from __future__ import annotations + +from contextlib import suppress import fnmatch from functools import partial import ipaddress import logging import socket +from typing import Any, TypedDict import voluptuous as vol from zeroconf import ( - DNSPointer, - DNSRecord, Error as ZeroconfError, InterfaceChoice, IPVersion, NonUniqueNameException, - ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf, @@ -21,29 +22,24 @@ from zeroconf import ( from homeassistant import util from homeassistant.const import ( - ATTR_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, __version__, ) +from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.singleton import singleton from homeassistant.loader import async_get_homekit, async_get_zeroconf +from .models import HaServiceBrowser, HaZeroconf from .usage import install_multiple_zeroconf_catcher _LOGGER = logging.getLogger(__name__) DOMAIN = "zeroconf" -ATTR_HOST = "host" -ATTR_PORT = "port" -ATTR_HOSTNAME = "hostname" -ATTR_TYPE = "type" -ATTR_PROPERTIES = "properties" - ZEROCONF_TYPE = "_home-assistant._tcp.local." HOMEKIT_TYPES = [ "_hap._tcp.local.", @@ -53,10 +49,9 @@ HOMEKIT_TYPES = [ CONF_DEFAULT_INTERFACE = "default_interface" CONF_IPV6 = "ipv6" -DEFAULT_DEFAULT_INTERFACE = False +DEFAULT_DEFAULT_INTERFACE = True DEFAULT_IPV6 = True -HOMEKIT_PROPERTIES = "properties" HOMEKIT_PAIRED_STATUS_FLAG = "sf" HOMEKIT_MODEL = "md" @@ -82,20 +77,31 @@ CONFIG_SCHEMA = vol.Schema( ) +class HaServiceInfo(TypedDict): + """Prepared info from mDNS entries.""" + + host: str + port: int | None + hostname: str + type: str + name: str + properties: dict[str, Any] + + @singleton(DOMAIN) -async def async_get_instance(hass): +async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" return await _async_get_instance(hass) -async def _async_get_instance(hass, **zcargs): +async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: logging.getLogger("zeroconf").setLevel(logging.NOTSET) zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs)) install_multiple_zeroconf_catcher(zeroconf) - def _stop_zeroconf(_): + def _stop_zeroconf(_event: Event) -> None: """Stop Zeroconf.""" zeroconf.ha_close() @@ -104,40 +110,10 @@ async def _async_get_instance(hass, **zcargs): return zeroconf -class HaServiceBrowser(ServiceBrowser): - """ServiceBrowser that only consumes DNSPointer records.""" - - def update_record(self, zc: "Zeroconf", now: float, record: DNSRecord) -> None: - """Pre-Filter update_record to DNSPointers for the configured type.""" - - # - # Each ServerBrowser currently runs in its own thread which - # processes every A or AAAA record update per instance. - # - # As the list of zeroconf names we watch for grows, each additional - # ServiceBrowser would process all the A and AAAA updates on the network. - # - # To avoid overwhemling the system we pre-filter here and only process - # DNSPointers for the configured record name (type) - # - if record.name not in self.types or not isinstance(record, DNSPointer): - return - super().update_record(zc, now, record) - - -class HaZeroconf(Zeroconf): - """Zeroconf that cannot be closed.""" - - def close(self): - """Fake method to avoid integrations closing it.""" - - ha_close = Zeroconf.close - - -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up Zeroconf and make Home Assistant discoverable.""" zc_config = config.get(DOMAIN, {}) - zc_args = {} + zc_args: dict = {} if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE): zc_args["interfaces"] = InterfaceChoice.Default if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): @@ -145,7 +121,7 @@ async def async_setup(hass, config): zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args) - async def _async_zeroconf_hass_start(_event): + async def _async_zeroconf_hass_start(_event: Event) -> None: """Expose Home Assistant on zeroconf when it starts. Wait till started or otherwise HTTP is not up and running. @@ -155,7 +131,7 @@ async def async_setup(hass, config): _register_hass_zc_service, hass, zeroconf, uuid ) - async def _async_zeroconf_hass_started(_event): + async def _async_zeroconf_hass_started(_event: Event) -> None: """Start the service browser.""" await _async_start_zeroconf_browser(hass, zeroconf) @@ -168,7 +144,9 @@ async def async_setup(hass, config): return True -def _register_hass_zc_service(hass, zeroconf, uuid): +def _register_hass_zc_service( + hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str +) -> None: # Get instance UUID valid_location_name = _truncate_location_name_to_valid(hass.config.location_name) @@ -185,15 +163,11 @@ def _register_hass_zc_service(hass, zeroconf, uuid): } # Get instance URL's - try: + with suppress(NoURLAvailableError): params["external_url"] = get_url(hass, allow_internal=False) - except NoURLAvailableError: - pass - try: + with suppress(NoURLAvailableError): params["internal_url"] = get_url(hass, allow_external=False) - except NoURLAvailableError: - pass # Set old base URL based on external or internal params["base_url"] = params["external_url"] or params["internal_url"] @@ -225,7 +199,9 @@ def _register_hass_zc_service(hass, zeroconf, uuid): ) -async def _async_start_zeroconf_browser(hass, zeroconf): +async def _async_start_zeroconf_browser( + hass: HomeAssistant, zeroconf: HaZeroconf +) -> None: """Start the zeroconf browser.""" zeroconf_types = await async_get_zeroconf(hass) @@ -237,12 +213,17 @@ async def _async_start_zeroconf_browser(hass, zeroconf): if hk_type not in zeroconf_types: types.append(hk_type) - def service_update(zeroconf, service_type, name, state_change): + def service_update( + zeroconf: Zeroconf, + service_type: str, + name: str, + state_change: ServiceStateChange, + ) -> None: """Service state changed.""" nonlocal zeroconf_types nonlocal homekit_models - if state_change != ServiceStateChange.Added: + if state_change == ServiceStateChange.Removed: return try: @@ -277,12 +258,11 @@ async def _async_start_zeroconf_browser(hass, zeroconf): # offering a second discovery for the same device if ( discovery_was_forwarded - and HOMEKIT_PROPERTIES in info - and HOMEKIT_PAIRED_STATUS_FLAG in info[HOMEKIT_PROPERTIES] + and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"] ): try: # 0 means paired and not discoverable by iOS clients) - if int(info[HOMEKIT_PROPERTIES][HOMEKIT_PAIRED_STATUS_FLAG]): + if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]): return except ValueError: # HomeKit pairing status unknown @@ -290,12 +270,12 @@ async def _async_start_zeroconf_browser(hass, zeroconf): return if "name" in info: - lowercase_name = info["name"].lower() + lowercase_name: str | None = info["name"].lower() else: lowercase_name = None - if "macaddress" in info.get("properties", {}): - uppercase_mac = info["properties"]["macaddress"].upper() + if "macaddress" in info["properties"]: + uppercase_mac: str | None = info["properties"]["macaddress"].upper() else: uppercase_mac = None @@ -319,20 +299,22 @@ async def _async_start_zeroconf_browser(hass, zeroconf): hass.add_job( hass.config_entries.flow.async_init( entry["domain"], context={"source": DOMAIN}, data=info - ) + ) # type: ignore ) _LOGGER.debug("Starting Zeroconf browser") HaServiceBrowser(zeroconf, types, handlers=[service_update]) -def handle_homekit(hass, homekit_models, info) -> bool: +def handle_homekit( + hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo +) -> bool: """Handle a HomeKit discovery. Return if discovery was forwarded. """ model = None - props = info.get(HOMEKIT_PROPERTIES, {}) + props = info["properties"] for key in props: if key.lower() == HOMEKIT_MODEL: @@ -353,16 +335,16 @@ def handle_homekit(hass, homekit_models, info) -> bool: hass.add_job( hass.config_entries.flow.async_init( homekit_models[test_model], context={"source": "homekit"}, data=info - ) + ) # type: ignore ) return True return False -def info_from_service(service): +def info_from_service(service: ServiceInfo) -> HaServiceInfo | None: """Return prepared info from mDNS entries.""" - properties = {"_raw": {}} + properties: dict[str, Any] = {"_raw": {}} for key, value in service.properties.items(): # See https://ietf.org/rfc/rfc6763.html#section-6.4 and @@ -378,30 +360,26 @@ def info_from_service(service): properties["_raw"][key] = value - try: + with suppress(UnicodeDecodeError): if isinstance(value, bytes): properties[key] = value.decode("utf-8") - except UnicodeDecodeError: - pass if not service.addresses: return None address = service.addresses[0] - info = { - ATTR_HOST: str(ipaddress.ip_address(address)), - ATTR_PORT: service.port, - ATTR_HOSTNAME: service.server, - ATTR_TYPE: service.type, - ATTR_NAME: service.name, - ATTR_PROPERTIES: properties, + return { + "host": str(ipaddress.ip_address(address)), + "port": service.port, + "hostname": service.server, + "type": service.type, + "name": service.name, + "properties": properties, } - return info - -def _suppress_invalid_properties(properties): +def _suppress_invalid_properties(properties: dict) -> None: """Suppress any properties that will cause zeroconf to fail to startup.""" for prop, prop_value in properties.items(): @@ -418,7 +396,7 @@ def _suppress_invalid_properties(properties): properties[prop] = "" -def _truncate_location_name_to_valid(location_name): +def _truncate_location_name_to_valid(location_name: str) -> str: """Truncate or return the location name usable for zeroconf.""" if len(location_name.encode("utf-8")) < MAX_NAME_LEN: return location_name diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 654eec820c3..d407acece57 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.8"], + "requirements": ["zeroconf==0.29.0"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py new file mode 100644 index 00000000000..02a6fc7cdaa --- /dev/null +++ b/homeassistant/components/zeroconf/models.py @@ -0,0 +1,33 @@ +"""Models for Zeroconf.""" + +from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf + + +class HaZeroconf(Zeroconf): + """Zeroconf that cannot be closed.""" + + def close(self) -> None: + """Fake method to avoid integrations closing it.""" + + ha_close = Zeroconf.close + + +class HaServiceBrowser(ServiceBrowser): + """ServiceBrowser that only consumes DNSPointer records.""" + + def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: + """Pre-Filter update_record to DNSPointers for the configured type.""" + + # + # Each ServerBrowser currently runs in its own thread which + # processes every A or AAAA record update per instance. + # + # As the list of zeroconf names we watch for grows, each additional + # ServiceBrowser would process all the A and AAAA updates on the network. + # + # To avoid overwhemling the system we pre-filter here and only process + # DNSPointers for the configured record name (type) + # + if record.name not in self.types or not isinstance(record, DNSPointer): + return + super().update_record(zc, now, record) diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py index 1303412249c..f7689ab63a4 100644 --- a/homeassistant/components/zeroconf/usage.py +++ b/homeassistant/components/zeroconf/usage.py @@ -1,6 +1,8 @@ """Zeroconf usage utility to warn about multiple instances.""" +from contextlib import suppress import logging +from typing import Any import zeroconf @@ -10,23 +12,25 @@ from homeassistant.helpers.frame import ( report_integration, ) +from .models import HaZeroconf + _LOGGER = logging.getLogger(__name__) -def install_multiple_zeroconf_catcher(hass_zc) -> None: +def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None: """Wrap the Zeroconf class to return the shared instance if multiple instances are detected.""" - def new_zeroconf_new(self, *k, **kw): + def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf: _report( "attempted to create another Zeroconf instance. Please use the shared Zeroconf via await homeassistant.components.zeroconf.async_get_instance(hass)", ) return hass_zc - def new_zeroconf_init(self, *k, **kw): + def new_zeroconf_init(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> None: return - zeroconf.Zeroconf.__new__ = new_zeroconf_new - zeroconf.Zeroconf.__init__ = new_zeroconf_init + zeroconf.Zeroconf.__new__ = new_zeroconf_new # type: ignore + zeroconf.Zeroconf.__init__ = new_zeroconf_init # type: ignore def _report(what: str) -> None: @@ -36,14 +40,12 @@ def _report(what: str) -> None: """ integration_frame = None - try: + with suppress(MissingIntegrationFrame): integration_frame = get_integration_frame(exclude_integrations={"zeroconf"}) - except MissingIntegrationFrame: - pass if not integration_frame: _LOGGER.warning( - "Detected code that %s. Please report this issue.", what, stack_info=True + "Detected code that %s; Please report this issue", what, stack_info=True ) return diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index 2c652f61c21..12953afeb2d 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -4,7 +4,7 @@ import asyncio from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN PLATFORMS = ["light"] @@ -20,9 +20,14 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Zerproc from a config entry.""" - for component in PLATFORMS: + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + 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, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) return True @@ -30,11 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + # Stop discovery + unregister_discovery = hass.data[DOMAIN].pop(DATA_DISCOVERY_SUBSCRIPTION, None) + if unregister_discovery: + unregister_discovery() + + hass.data.pop(DOMAIN, None) + return all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/zerproc/const.py b/homeassistant/components/zerproc/const.py index a5481bd4c34..69d5fcfb740 100644 --- a/homeassistant/components/zerproc/const.py +++ b/homeassistant/components/zerproc/const.py @@ -1,2 +1,5 @@ """Constants for the Zerproc integration.""" DOMAIN = "zerproc" + +DATA_ADDRESSES = "addresses" +DATA_DISCOVERY_SUBSCRIPTION = "discovery_subscription" diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 89f60faf84e..627358ab971 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -1,8 +1,9 @@ """Zerproc light platform.""" -import asyncio +from __future__ import annotations + from datetime import timedelta import logging -from typing import Callable, List, Optional +from typing import Callable import pyzerproc @@ -21,7 +22,7 @@ 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 DOMAIN +from .const import DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,34 +31,21 @@ SUPPORT_ZERPROC = SUPPORT_BRIGHTNESS | SUPPORT_COLOR DISCOVERY_INTERVAL = timedelta(seconds=60) -async def connect_light(light: pyzerproc.Light) -> Optional[pyzerproc.Light]: - """Return the given light if it connects successfully.""" - try: - await light.connect() - except pyzerproc.ZerprocException: - _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) - return None - return light - - -async def discover_entities(hass: HomeAssistant) -> List[Entity]: +async def discover_entities(hass: HomeAssistant) -> list[Entity]: """Attempt to discover new lights.""" lights = await pyzerproc.discover() # Filter out already discovered lights new_lights = [ - light for light in lights if light.address not in hass.data[DOMAIN]["addresses"] + light + for light in lights + if light.address not in hass.data[DOMAIN][DATA_ADDRESSES] ] entities = [] - connected_lights = filter( - None, await asyncio.gather(*(connect_light(light) for light in new_lights)) - ) - for light in connected_lights: - # Double-check the light hasn't been added in the meantime - if light.address not in hass.data[DOMAIN]["addresses"]: - hass.data[DOMAIN]["addresses"].add(light.address) - entities.append(ZerprocLight(light)) + for light in new_lights: + hass.data[DOMAIN][DATA_ADDRESSES].add(light.address) + entities.append(ZerprocLight(light)) return entities @@ -65,14 +53,9 @@ async def discover_entities(hass: HomeAssistant) -> List[Entity]: async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, - async_add_entities: Callable[[List[Entity], bool], None], + async_add_entities: Callable[[list[Entity], bool], None], ) -> None: """Set up Zerproc light devices.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - if "addresses" not in hass.data[DOMAIN]: - hass.data[DOMAIN]["addresses"] = set() - warned = False async def discover(*args): @@ -91,7 +74,9 @@ async def async_setup_entry( hass.async_create_task(discover()) # Perform recurring discovery of new devices - async_track_time_interval(hass, discover, DISCOVERY_INTERVAL) + hass.data[DOMAIN][DATA_DISCOVERY_SUBSCRIPTION] = async_track_time_interval( + hass, discover, DISCOVERY_INTERVAL + ) class ZerprocLight(LightEntity): @@ -120,7 +105,7 @@ class ZerprocLight(LightEntity): await self._light.disconnect() except pyzerproc.ZerprocException: _LOGGER.debug( - "Exception disconnected from %s", self.entity_id, exc_info=True + "Exception disconnecting from %s", self._light.address, exc_info=True ) @property @@ -143,7 +128,7 @@ class ZerprocLight(LightEntity): } @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon to use in the frontend.""" return "mdi:string-lights" @@ -198,11 +183,11 @@ class ZerprocLight(LightEntity): state = await self._light.get_state() except pyzerproc.ZerprocException: if self._available: - _LOGGER.warning("Unable to connect to %s", self.entity_id) + _LOGGER.warning("Unable to connect to %s", self._light.address) self._available = False return if self._available is False: - _LOGGER.info("Reconnected to %s", self.entity_id) + _LOGGER.info("Reconnected to %s", self._light.address) self._available = True self._is_on = state.is_on hsv = color_util.color_RGB_to_hsv(*state.color) diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index 54b70d78673..d2d00987ab7 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": [ - "pyzerproc==0.4.7" + "pyzerproc==0.4.8" ], "codeowners": [ "@emlove" diff --git a/homeassistant/components/zerproc/translations/id.json b/homeassistant/components/zerproc/translations/id.json new file mode 100644 index 00000000000..223836a8b40 --- /dev/null +++ b/homeassistant/components/zerproc/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/ko.json b/homeassistant/components/zerproc/translations/ko.json index 7011a61f757..e5ae04d6e5c 100644 --- a/homeassistant/components/zerproc/translations/ko.json +++ b/homeassistant/components/zerproc/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "confirm": { diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index cdf7e6304ad..0333bb76a20 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -6,10 +6,9 @@ import requests import voluptuous as vol import xmltodict -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = "http://www.zillow.com/webservice/GetZestimate.htm" @@ -56,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors, True) -class ZestimateDataSensor(Entity): +class ZestimateDataSensor(SensorEntity): """Implementation of a Zestimate sensor.""" def __init__(self, name, params): @@ -86,7 +85,7 @@ class ZestimateDataSensor(Entity): return None @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" attributes = {} if self.data is not None: diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index d5f76fa5e23..707e0292c45 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -4,6 +4,7 @@ import asyncio import logging import voluptuous as vol +from zhaquirks import setup as setup_quirks from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries, const as ha_const @@ -16,7 +17,6 @@ from . import api from .core import ZHAGateway from .core.const import ( BAUD_RATES, - COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, @@ -30,6 +30,7 @@ from .core.const import ( DATA_ZHA_GATEWAY, DATA_ZHA_PLATFORM_LOADED, DOMAIN, + PLATFORMS, SIGNAL_ADD_ENTITIES, RadioType, ) @@ -88,21 +89,19 @@ async def async_setup_entry(hass, config_entry): zha_data = hass.data.setdefault(DATA_ZHA, {}) config = zha_data.get(DATA_ZHA_CONFIG, {}) - for component in COMPONENTS: - zha_data.setdefault(component, []) + for platform in PLATFORMS: + zha_data.setdefault(platform, []) if config.get(CONF_ENABLE_QUIRKS, True): - # needs to be done here so that the ZHA module is finished loading - # before zhaquirks is imported - import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error + setup_quirks(config) zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() zha_data[DATA_ZHA_DISPATCHERS] = [] zha_data[DATA_ZHA_PLATFORM_LOADED] = [] - for component in COMPONENTS: - coro = hass.config_entries.async_forward_entry_setup(config_entry, component) + for platform in PLATFORMS: + coro = hass.config_entries.async_forward_entry_setup(config_entry, platform) zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro)) device_registry = await hass.helpers.device_registry.async_get_registry() @@ -138,8 +137,8 @@ async def async_unload_entry(hass, config_entry): for unsub_dispatcher in dispatchers: unsub_dispatcher() - for component in COMPONENTS: - await hass.config_entries.async_forward_entry_unload(config_entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, platform) return True diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 2bd712ff681..7e265d03c09 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -11,6 +11,7 @@ from zigpy.types.named import EUI64 import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api +from homeassistant.const import ATTR_COMMAND, ATTR_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -20,14 +21,12 @@ from .core.const import ( ATTR_ATTRIBUTE, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, - ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, ATTR_IEEE, ATTR_LEVEL, ATTR_MANUFACTURER, ATTR_MEMBERS, - ATTR_NAME, ATTR_VALUE, ATTR_WARNING_DEVICE_DURATION, ATTR_WARNING_DEVICE_MODE, @@ -61,6 +60,7 @@ from .core.helpers import ( get_matched_clusters, qr_to_install_code, ) +from .core.typing import ZhaDeviceType, ZhaGatewayType _LOGGER = logging.getLogger(__name__) @@ -462,9 +462,38 @@ async def websocket_remove_group_members(hass, connection, msg): ) async def websocket_reconfigure_node(hass, connection, msg): """Reconfigure a ZHA nodes entities by its ieee address.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] - device = zha_gateway.get_device(ieee) + device: ZhaDeviceType = zha_gateway.get_device(ieee) + ieee_str = str(device.ieee) + nwk_str = device.nwk.__repr__() + + class DeviceLogFilterer(logging.Filter): + """Log filterer that limits messages to the specified device.""" + + def filter(self, record): + message = record.getMessage() + return nwk_str in message or ieee_str in message + + filterer = DeviceLogFilterer() + + async def forward_messages(data): + """Forward events to websocket.""" + connection.send_message(websocket_api.event_message(msg["id"], data)) + + remove_dispatcher_function = async_dispatcher_connect( + hass, "zha_gateway_message", forward_messages + ) + + @callback + def async_cleanup() -> None: + """Remove signal listener and turn off debug mode.""" + zha_gateway.async_disable_debug_mode(filterer=filterer) + remove_dispatcher_function() + + connection.subscriptions[msg["id"]] = async_cleanup + zha_gateway.async_enable_debug_mode(filterer=filterer) + _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee) hass.async_create_task(device.async_configure()) @@ -893,9 +922,12 @@ def async_load_api(hass): async def remove(service): """Remove a node from the network.""" ieee = service.data[ATTR_IEEE] - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - zha_device = zha_gateway.get_device(ieee) - if zha_device is not None and zha_device.is_coordinator: + zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_device: ZhaDeviceType = zha_gateway.get_device(ieee) + if zha_device is not None and ( + zha_device.is_coordinator + and zha_device.ieee == zha_gateway.application_controller.ieee + ): _LOGGER.info("Removing the coordinator (%s) is not allowed", ieee) return _LOGGER.info("Removing node %s", ieee) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index ab0f15f7559..475b0c5d0b8 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -4,11 +4,12 @@ Climate on Zigbee Home Automation networks. For more details on this platform, please refer to the documentation at https://home-assistant.io/components/zha.climate/ """ +from __future__ import annotations + from datetime import datetime, timedelta import enum import functools from random import randint -from typing import List, Optional, Tuple from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -30,6 +31,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, PRESET_NONE, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, @@ -48,6 +52,8 @@ from .core.const import ( CHANNEL_THERMOSTAT, DATA_ZHA, DATA_ZHA_DISPATCHERS, + PRESET_COMPLEX, + PRESET_SCHEDULE, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -185,7 +191,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return self._thrm.local_temp / ZCL_TEMP @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device specific state attributes.""" data = {} if self.hvac_mode: @@ -212,7 +218,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return data @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return current FAN mode.""" if self._thrm.running_state is None: return FAN_AUTO @@ -224,14 +230,14 @@ class Thermostat(ZhaEntity, ClimateEntity): return FAN_AUTO @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return supported FAN modes.""" if not self._fan: return None return [FAN_AUTO, FAN_ON] @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current HVAC action.""" if ( self._thrm.pi_heating_demand is None @@ -241,7 +247,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return self._pi_demand_action @property - def _rm_rs_action(self) -> Optional[str]: + def _rm_rs_action(self) -> str | None: """Return the current HVAC action based on running mode and running state.""" running_mode = self._thrm.running_mode @@ -260,7 +266,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return CURRENT_HVAC_OFF @property - def _pi_demand_action(self) -> Optional[str]: + def _pi_demand_action(self) -> str | None: """Return the current HVAC action based on pi_demands.""" heating_demand = self._thrm.pi_heating_demand @@ -275,12 +281,12 @@ class Thermostat(ZhaEntity, ClimateEntity): return CURRENT_HVAC_OFF @property - def hvac_mode(self) -> Optional[str]: + def hvac_mode(self) -> str | None: """Return HVAC operation mode.""" return SYSTEM_MODE_2_HVAC.get(self._thrm.system_mode) @property - def hvac_modes(self) -> Tuple[str, ...]: + def hvac_modes(self) -> tuple[str, ...]: """Return the list of available HVAC operation modes.""" return SEQ_OF_OPERATION.get(self._thrm.ctrl_seqe_of_oper, (HVAC_MODE_OFF,)) @@ -290,12 +296,12 @@ class Thermostat(ZhaEntity, ClimateEntity): return PRECISION_TENTHS @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return current preset mode.""" return self._preset @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return supported preset modes.""" return self._presets @@ -442,15 +448,22 @@ class Thermostat(ZhaEntity, ClimateEntity): self.debug("preset mode '%s' is not supported", preset_mode) return - if self.preset_mode not in (preset_mode, PRESET_NONE): - if not await self.async_preset_handler(self.preset_mode, enable=False): - self.debug("Couldn't turn off '%s' preset", self.preset_mode) - return + if ( + self.preset_mode + not in ( + preset_mode, + PRESET_NONE, + ) + and not await self.async_preset_handler(self.preset_mode, enable=False) + ): + self.debug("Couldn't turn off '%s' preset", self.preset_mode) + return - if preset_mode != PRESET_NONE: - if not await self.async_preset_handler(preset_mode, enable=True): - self.debug("Couldn't turn on '%s' preset", preset_mode) - return + if preset_mode != PRESET_NONE and not await self.async_preset_handler( + preset_mode, enable=True + ): + self.debug("Couldn't turn on '%s' preset", preset_mode) + return self._preset = preset_mode self.async_write_ha_state() @@ -566,7 +579,7 @@ class ZenWithinThermostat(Thermostat): """Zen Within Thermostat implementation.""" @property - def _rm_rs_action(self) -> Optional[str]: + def _rm_rs_action(self) -> str | None: """Return the current HVAC action based on running mode and running state.""" running_state = self._thrm.running_state @@ -594,3 +607,88 @@ class ZenWithinThermostat(Thermostat): ) class CentralitePearl(ZenWithinThermostat): """Centralite Pearl Thermostat implementation.""" + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers={ + "_TZE200_ckud7u2l", + "_TZE200_ywdxldoj", + "_TYST11_ckud7u2l", + "_TYST11_ywdxldoj", + }, +) +class MoesThermostat(Thermostat): + """Moes Thermostat implementation.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize ZHA Thermostat instance.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._presets = [ + PRESET_NONE, + PRESET_AWAY, + PRESET_SCHEDULE, + PRESET_COMFORT, + PRESET_ECO, + PRESET_BOOST, + PRESET_COMPLEX, + ] + self._supported_flags |= SUPPORT_PRESET_MODE + + @property + def hvac_modes(self) -> tuple[str, ...]: + """Return only the heat mode, because the device can't be turned off.""" + return (HVAC_MODE_HEAT,) + + async def async_attribute_updated(self, record): + """Handle attribute update from device.""" + if record.attr_name == "operation_preset": + if record.value == 0: + self._preset = PRESET_AWAY + if record.value == 1: + self._preset = PRESET_SCHEDULE + if record.value == 2: + self._preset = PRESET_NONE + if record.value == 3: + self._preset = PRESET_COMFORT + if record.value == 4: + self._preset = PRESET_ECO + if record.value == 5: + self._preset = PRESET_BOOST + if record.value == 6: + self._preset = PRESET_COMPLEX + await super().async_attribute_updated(record) + + async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + """Set the preset mode.""" + mfg_code = self._zha_device.manufacturer_code + if not enable: + return await self._thrm.write_attributes( + {"operation_preset": 2}, manufacturer=mfg_code + ) + if preset == PRESET_AWAY: + return await self._thrm.write_attributes( + {"operation_preset": 0}, manufacturer=mfg_code + ) + if preset == PRESET_SCHEDULE: + return await self._thrm.write_attributes( + {"operation_preset": 1}, manufacturer=mfg_code + ) + if preset == PRESET_COMFORT: + return await self._thrm.write_attributes( + {"operation_preset": 3}, manufacturer=mfg_code + ) + if preset == PRESET_ECO: + return await self._thrm.write_attributes( + {"operation_preset": 4}, manufacturer=mfg_code + ) + if preset == PRESET_BOOST: + return await self._thrm.write_attributes( + {"operation_preset": 5}, manufacturer=mfg_code + ) + if preset == PRESET_COMPLEX: + return await self._thrm.write_attributes( + {"operation_preset": 6}, manufacturer=mfg_code + ) + + return False diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index f59f53c7995..9c440c29cd3 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,14 +1,18 @@ """Config flow for ZHA.""" +from __future__ import annotations + import os -from typing import Any, Dict, Optional +from typing import Any import serial.tools.list_ports import voluptuous as vol from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.typing import DiscoveryInfoType -from .core.const import ( # pylint:disable=unused-import +from .core.const import ( CONF_BAUDRATE, CONF_FLOWCONTROL, CONF_RADIO_TYPE, @@ -89,6 +93,37 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle zeroconf discovery.""" + # Hostname is format: livingroom.local. + local_name = discovery_info["hostname"][:-1] + node_name = local_name[: -len(".local")] + host = discovery_info[CONF_HOST] + device_path = f"socket://{host}:6638" + + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured( + updates={ + CONF_DEVICE: {CONF_DEVICE_PATH: device_path}, + } + ) + + # Check if already configured + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_NAME: node_name, + } + + self._device_path = device_path + self._radio_type = ( + RadioType.ezsp.name if "efr32" in local_name else RadioType.znp.name + ) + + return await self.async_step_port_config() + async def async_step_port_config(self, user_input=None): """Enter port settings specific for this type of radio.""" errors = {} @@ -116,9 +151,13 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if isinstance(radio_schema, vol.Schema): radio_schema = radio_schema.schema + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + source = self.context.get("source") for param, value in radio_schema.items(): if param in SUPPORTED_PORT_SETTINGS: schema[param] = value + if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE: + schema[param] = 115200 return self.async_show_form( step_id="port_config", @@ -127,7 +166,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def detect_radios(dev_path: str) -> Optional[Dict[str, Any]]: +async def detect_radios(dev_path: str) -> dict[str, Any] | None: """Probe all radio types on the device port.""" for radio in RadioType: dev_config = radio.controller.SCHEMA_DEVICE({CONF_DEVICE_PATH: dev_path}) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 852d576c035..289f1c36d4d 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict import zigpy.zcl.clusters.closures @@ -10,7 +10,7 @@ from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import ( # noqa: F401 # pylint: disable=unused-import +from . import ( # noqa: F401 base, closures, general, @@ -40,7 +40,7 @@ class Channels: def __init__(self, zha_device: zha_typing.ZhaDeviceType) -> None: """Initialize instance.""" - self._pools: List[zha_typing.ChannelPoolType] = [] + self._pools: list[zha_typing.ChannelPoolType] = [] self._power_config = None self._identify = None self._semaphore = asyncio.Semaphore(3) @@ -49,7 +49,7 @@ class Channels: self._zha_device = zha_device @property - def pools(self) -> List[ChannelPool]: + def pools(self) -> list[ChannelPool]: """Return channel pools list.""" return self._pools @@ -96,7 +96,7 @@ class Channels: return self._unique_id @property - def zigbee_signature(self) -> Dict[int, Dict[str, Any]]: + def zigbee_signature(self) -> dict[int, dict[str, Any]]: """Get the zigbee signatures for the pools in channels.""" return { signature[0]: signature[1] @@ -137,7 +137,7 @@ class Channels: component: str, entity_class: zha_typing.CALLABLE_T, unique_id: str, - channels: List[zha_typing.ChannelType], + channels: list[zha_typing.ChannelType], ): """Signal new entity addition.""" if self.zha_device.status == zha_core_device.DeviceStatus.INITIALIZED: @@ -153,7 +153,7 @@ class Channels: async_dispatcher_send(self.zha_device.hass, signal, *args) @callback - def zha_send_event(self, event_data: Dict[str, Union[str, int]]) -> None: + def zha_send_event(self, event_data: dict[str, str | int]) -> None: """Relay events to hass.""" self.zha_device.hass.bus.async_fire( "zha_event", @@ -175,7 +175,7 @@ class ChannelPool: self._channels: Channels = channels self._claimed_channels: ChannelsDict = {} self._id: int = ep_id - self._client_channels: Dict[str, zha_typing.ClientChannelType] = {} + self._client_channels: dict[str, zha_typing.ClientChannelType] = {} self._unique_id: str = f"{channels.unique_id}-{ep_id}" @property @@ -189,7 +189,7 @@ class ChannelPool: return self._claimed_channels @property - def client_channels(self) -> Dict[str, zha_typing.ClientChannelType]: + def client_channels(self) -> dict[str, zha_typing.ClientChannelType]: """Return a dict of client channels.""" return self._client_channels @@ -214,12 +214,12 @@ class ChannelPool: return self._channels.zha_device.is_mains_powered @property - def manufacturer(self) -> Optional[str]: + def manufacturer(self) -> str | None: """Return device manufacturer.""" return self._channels.zha_device.manufacturer @property - def manufacturer_code(self) -> Optional[int]: + def manufacturer_code(self) -> int | None: """Return device manufacturer.""" return self._channels.zha_device.manufacturer_code @@ -229,7 +229,7 @@ class ChannelPool: return self._channels.zha_device.hass @property - def model(self) -> Optional[str]: + def model(self) -> str | None: """Return device model.""" return self._channels.zha_device.model @@ -244,7 +244,7 @@ class ChannelPool: return self._unique_id @property - def zigbee_signature(self) -> Tuple[int, Dict[str, Any]]: + def zigbee_signature(self) -> tuple[int, dict[str, Any]]: """Get the zigbee signature for the endpoint this pool represents.""" return ( self.endpoint.endpoint_id, @@ -342,7 +342,7 @@ class ChannelPool: component: str, entity_class: zha_typing.CALLABLE_T, unique_id: str, - channels: List[zha_typing.ChannelType], + channels: list[zha_typing.ChannelType], ): """Signal new entity addition.""" self._channels.async_new_entity(component, entity_class, unique_id, channels) @@ -353,19 +353,19 @@ class ChannelPool: self._channels.async_send_signal(signal, *args) @callback - def claim_channels(self, channels: List[zha_typing.ChannelType]) -> None: + def claim_channels(self, channels: list[zha_typing.ChannelType]) -> None: """Claim a channel.""" self.claimed_channels.update({ch.id: ch for ch in channels}) @callback - def unclaimed_channels(self) -> List[zha_typing.ChannelType]: + def unclaimed_channels(self) -> list[zha_typing.ChannelType]: """Return a list of available (unclaimed) channels.""" claimed = set(self.claimed_channels) available = set(self.all_channels) return [self.all_channels[chan_id] for chan_id in (available - claimed)] @callback - def zha_send_event(self, event_data: Dict[str, Union[str, int]]) -> None: + def zha_send_event(self, event_data: dict[str, str | int]) -> None: """Relay events to hass.""" self._channels.zha_send_event( { diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 2dbd1629487..bc93459dbad 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -1,13 +1,15 @@ """Base classes for channels.""" +from __future__ import annotations import asyncio from enum import Enum from functools import wraps import logging -from typing import Any, Union +from typing import Any import zigpy.exceptions +from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback from .. import typing as zha_typing @@ -16,7 +18,6 @@ from ..const import ( ATTR_ATTRIBUTE_ID, ATTR_ATTRIBUTE_NAME, ATTR_CLUSTER_ID, - ATTR_COMMAND, ATTR_UNIQUE_ID, ATTR_VALUE, CHANNEL_ZDO, @@ -238,7 +239,7 @@ class ZigbeeChannel(LogMixin): """Handle ZDO commands on this cluster.""" @callback - def zha_send_event(self, command: str, args: Union[int, dict]) -> None: + def zha_send_event(self, command: str, args: int | dict) -> None: """Relay events to hass.""" self._ch_pool.zha_send_event( { diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 0326f18ac69..e427bc962b4 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -23,6 +23,27 @@ class DoorLockChannel(ZigbeeChannel): f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "lock_state", result ) + @callback + def cluster_command(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + + if ( + self._cluster.client_commands is None + or self._cluster.client_commands.get(command_id) is None + ): + return + + command_name = self._cluster.client_commands.get(command_id, [command_id])[0] + if command_name == "operation_event_notification": + self.zha_send_event( + command_name, + { + "source": args[0].name, + "operation": args[1].name, + "code_slot": (args[2] + 1), # start code slots at 1 + }, + ) + @callback def attribute_updated(self, attrid, value): """Handle attribute update from lock cluster.""" @@ -35,6 +56,53 @@ class DoorLockChannel(ZigbeeChannel): f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) + async def async_set_user_code(self, code_slot: int, user_code: str) -> None: + """Set the user code for the code slot.""" + + await self.set_pin_code( + code_slot - 1, # start code slots at 1, Zigbee internals use 0 + closures.DoorLock.UserStatus.Enabled, + closures.DoorLock.UserType.Unrestricted, + user_code, + ) + + async def async_enable_user_code(self, code_slot: int) -> None: + """Enable the code slot.""" + + await self.set_user_status(code_slot - 1, closures.DoorLock.UserStatus.Enabled) + + async def async_disable_user_code(self, code_slot: int) -> None: + """Disable the code slot.""" + + await self.set_user_status(code_slot - 1, closures.DoorLock.UserStatus.Disabled) + + async def async_get_user_code(self, code_slot: int) -> int: + """Get the user code from the code slot.""" + + result = await self.get_pin_code(code_slot - 1) + return result + + async def async_clear_user_code(self, code_slot: int) -> None: + """Clear the code slot.""" + + await self.clear_pin_code(code_slot - 1) + + async def async_clear_all_user_codes(self) -> None: + """Clear all code slots.""" + + await self.clear_all_pin_codes() + + async def async_set_user_type(self, code_slot: int, user_type: str) -> None: + """Set user type.""" + + await self.set_user_type(code_slot - 1, user_type) + + async def async_get_user_type(self, code_slot: int) -> str: + """Get user type.""" + + result = await self.get_user_type(code_slot - 1) + return result + @registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) class Shade(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index d105572c182..6ef0bd9e665 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -1,6 +1,8 @@ """General channels module for Zigbee Home Automation.""" +from __future__ import annotations + import asyncio -from typing import Any, Coroutine, List, Optional +from typing import Any, Coroutine import zigpy.exceptions import zigpy.zcl.clusters.general as general @@ -44,42 +46,42 @@ class AnalogOutput(ZigbeeChannel): REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] @property - def present_value(self) -> Optional[float]: + def present_value(self) -> float | None: """Return cached value of present_value.""" return self.cluster.get("present_value") @property - def min_present_value(self) -> Optional[float]: + def min_present_value(self) -> float | None: """Return cached value of min_present_value.""" return self.cluster.get("min_present_value") @property - def max_present_value(self) -> Optional[float]: + def max_present_value(self) -> float | None: """Return cached value of max_present_value.""" return self.cluster.get("max_present_value") @property - def resolution(self) -> Optional[float]: + def resolution(self) -> float | None: """Return cached value of resolution.""" return self.cluster.get("resolution") @property - def relinquish_default(self) -> Optional[float]: + def relinquish_default(self) -> float | None: """Return cached value of relinquish_default.""" return self.cluster.get("relinquish_default") @property - def description(self) -> Optional[str]: + def description(self) -> str | None: """Return cached value of description.""" return self.cluster.get("description") @property - def engineering_units(self) -> Optional[int]: + def engineering_units(self) -> int | None: """Return cached value of engineering_units.""" return self.cluster.get("engineering_units") @property - def application_type(self) -> Optional[int]: + def application_type(self) -> int | None: """Return cached value of application_type.""" return self.cluster.get("application_type") @@ -91,7 +93,7 @@ class AnalogOutput(ZigbeeChannel): self.error("Could not set value: %s", ex) return False if isinstance(res, list) and all( - [record.status == Status.SUCCESS for record in res[0]] + record.status == Status.SUCCESS for record in res[0] ): return True return False @@ -215,7 +217,7 @@ class LevelControlChannel(ZigbeeChannel): REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) @property - def current_level(self) -> Optional[int]: + def current_level(self) -> int | None: """Return cached value of the current_level attribute.""" return self.cluster.get("current_level") @@ -293,7 +295,7 @@ class OnOffChannel(ZigbeeChannel): self._off_listener = None @property - def on_off(self) -> Optional[bool]: + def on_off(self) -> bool | None: """Return cached value of on/off attribute.""" return self.cluster.get("on_off") @@ -367,7 +369,7 @@ class Ota(ZigbeeChannel): @callback def cluster_command( - self, tsn: int, command_id: int, args: Optional[List[Any]] + self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle OTA commands.""" cmd_name = self.cluster.server_commands.get(command_id, [command_id])[0] @@ -389,6 +391,9 @@ class PollControl(ZigbeeChannel): CHECKIN_INTERVAL = 55 * 60 * 4 # 55min CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s LONG_POLL = 6 * 4 # 6s + _IGNORED_MANUFACTURER_ID = { + 4476, + } # IKEA async def async_configure_channel_specific(self) -> None: """Configure channel: set check-in interval.""" @@ -402,7 +407,7 @@ class PollControl(ZigbeeChannel): @callback def cluster_command( - self, tsn: int, command_id: int, args: Optional[List[Any]] + self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle commands received to this cluster.""" cmd_name = self.cluster.client_commands.get(command_id, [command_id])[0] @@ -414,7 +419,13 @@ class PollControl(ZigbeeChannel): async def check_in_response(self, tsn: int) -> None: """Respond to checkin command.""" await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn) - await self.set_long_poll_interval(self.LONG_POLL) + if self._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID: + await self.set_long_poll_interval(self.LONG_POLL) + + @callback + def skip_manufacturer_id(self, manufacturer_code: int) -> None: + """Block a specific manufacturer id from changing default polling.""" + self._IGNORED_MANUFACTURER_ID.add(manufacturer_code) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 5b3a4778fcd..989cc17f97d 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -1,5 +1,7 @@ """Home automation channels module for Zigbee Home Automation.""" -from typing import Coroutine, Optional +from __future__ import annotations + +from typing import Coroutine import zigpy.zcl.clusters.homeautomation as homeautomation @@ -76,14 +78,14 @@ class ElectricalMeasurementChannel(ZigbeeChannel): ) @property - def divisor(self) -> Optional[int]: + def divisor(self) -> int | None: """Return active power divisor.""" return self.cluster.get( "ac_power_divisor", self.cluster.get("power_divisor", 1) ) @property - def multiplier(self) -> Optional[int]: + def multiplier(self) -> int | None: """Return active power divisor.""" return self.cluster.get( "ac_power_multiplier", self.cluster.get("power_multiplier", 1) diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 1647c5ce52d..76f9c0b4e80 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -4,9 +4,11 @@ HVAC channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ +from __future__ import annotations + import asyncio from collections import namedtuple -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.hvac as hvac @@ -44,7 +46,7 @@ class FanChannel(ZigbeeChannel): REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) @property - def fan_mode(self) -> Optional[int]: + def fan_mode(self) -> int | None: """Return current fan mode.""" return self.cluster.get("fan_mode") @@ -198,22 +200,22 @@ class ThermostatChannel(ZigbeeChannel): return self._min_heat_setpoint_limit @property - def local_temp(self) -> Optional[int]: + def local_temp(self) -> int | None: """Thermostat temperature.""" return self._local_temp @property - def occupancy(self) -> Optional[int]: + def occupancy(self) -> int | None: """Is occupancy detected.""" return self._occupancy @property - def occupied_cooling_setpoint(self) -> Optional[int]: + def occupied_cooling_setpoint(self) -> int | None: """Temperature when room is occupied.""" return self._occupied_cooling_setpoint @property - def occupied_heating_setpoint(self) -> Optional[int]: + def occupied_heating_setpoint(self) -> int | None: """Temperature when room is occupied.""" return self._occupied_heating_setpoint @@ -228,27 +230,27 @@ class ThermostatChannel(ZigbeeChannel): return self._pi_heating_demand @property - def running_mode(self) -> Optional[int]: + def running_mode(self) -> int | None: """Thermostat running mode.""" return self._running_mode @property - def running_state(self) -> Optional[int]: + def running_state(self) -> int | None: """Thermostat running state, state of heat, cool, fan relays.""" return self._running_state @property - def system_mode(self) -> Optional[int]: + def system_mode(self) -> int | None: """System mode.""" return self._system_mode @property - def unoccupied_cooling_setpoint(self) -> Optional[int]: + def unoccupied_cooling_setpoint(self) -> int | None: """Temperature when room is not occupied.""" return self._unoccupied_cooling_setpoint @property - def unoccupied_heating_setpoint(self) -> Optional[int]: + def unoccupied_heating_setpoint(self) -> int | None: """Temperature when room is not occupied.""" return self._unoccupied_heating_setpoint @@ -309,7 +311,7 @@ class ThermostatChannel(ZigbeeChannel): chunk, rest = rest[:4], rest[4:] def _configure_reporting_status( - self, attrs: Dict[Union[int, str], Tuple], res: Union[List, Tuple] + self, attrs: dict[int | str, tuple], res: list | tuple ) -> None: """Parse configure reporting result.""" if not isinstance(res, list): @@ -405,7 +407,7 @@ class ThermostatChannel(ZigbeeChannel): self.debug("set cooling setpoint to %s", temperature) return True - async def get_occupancy(self) -> Optional[bool]: + async def get_occupancy(self) -> bool | None: """Get unreportable occupancy attribute.""" try: res, fail = await self.cluster.read_attributes(["occupancy"]) @@ -434,7 +436,7 @@ class ThermostatChannel(ZigbeeChannel): if not isinstance(res, list): return False - return all([record.status == Status.SUCCESS for record in res[0]]) + return all(record.status == Status.SUCCESS for record in res[0]) @registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index c8827e20e01..eef4c56e379 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,5 +1,8 @@ """Lighting channels module for Zigbee Home Automation.""" -from typing import Coroutine, Optional +from __future__ import annotations + +from contextlib import suppress +from typing import Coroutine import zigpy.zcl.clusters.lighting as lighting @@ -37,31 +40,29 @@ class ColorChannel(ZigbeeChannel): @property def color_capabilities(self) -> int: """Return color capabilities of the light.""" - try: + with suppress(KeyError): return self.cluster["color_capabilities"] - except KeyError: - pass if self.cluster.get("color_temperature") is not None: return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP return self.CAPABILITIES_COLOR_XY @property - def color_loop_active(self) -> Optional[int]: + def color_loop_active(self) -> int | None: """Return cached value of the color_loop_active attribute.""" return self.cluster.get("color_loop_active") @property - def color_temperature(self) -> Optional[int]: + def color_temperature(self) -> int | None: """Return cached value of color temperature.""" return self.cluster.get("color_temperature") @property - def current_x(self) -> Optional[int]: + def current_x(self) -> int | None: """Return cached value of the current_x attribute.""" return self.cluster.get("current_x") @property - def current_y(self) -> Optional[int]: + def current_y(self) -> int | None: """Return cached value of the current_y attribute.""" return self.cluster.get("current_y") diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 64db1aa82ac..78ff12a9bf3 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -74,3 +74,31 @@ class TemperatureMeasurement(ZigbeeChannel): "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), } ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.CarbonMonoxideConcentration.cluster_id +) +class CarbonMonoxideConcentration(ZigbeeChannel): + """Carbon Monoxide measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), + } + ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.CarbonDioxideConcentration.cluster_id +) +class CarbonDioxideConcentration(ZigbeeChannel): + """Carbon Dioxide measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), + } + ] diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 32e4902799e..a815c75c8b3 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -1,5 +1,7 @@ """Smart energy channels module for Zigbee Home Automation.""" -from typing import Coroutine, Union +from __future__ import annotations + +from typing import Coroutine import zigpy.zcl.clusters.smartenergy as smartenergy @@ -139,7 +141,7 @@ class Metering(ZigbeeChannel): else: self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}" - def formatter_function(self, value: int) -> Union[int, float]: + def formatter_function(self, value: int) -> int | float: """Return formatted value for display.""" value = value * self.multiplier / self.divisor if self.unit_of_measurement == POWER_WATT: diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 1d3f767353b..2c968a5f02d 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -1,7 +1,8 @@ """All constants related to the ZHA component.""" +from __future__ import annotations + import enum import logging -from typing import List import bellows.zigbee.application from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import @@ -31,7 +32,6 @@ ATTR_ATTRIBUTE_NAME = "attribute_name" ATTR_AVAILABLE = "available" ATTR_CLUSTER_ID = "cluster_id" ATTR_CLUSTER_TYPE = "cluster_type" -ATTR_COMMAND = "command" ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE_IEEE = "device_ieee" ATTR_DEVICE_TYPE = "device_type" @@ -47,7 +47,6 @@ ATTR_MANUFACTURER = "manufacturer" ATTR_MANUFACTURER_CODE = "manufacturer_code" ATTR_MEMBERS = "members" ATTR_MODEL = "model" -ATTR_NAME = "name" ATTR_NEIGHBORS = "neighbors" ATTR_NODE_DESCRIPTOR = "node_descriptor" ATTR_NWK = "nwk" @@ -104,7 +103,7 @@ CLUSTER_COMMANDS_SERVER = "server_commands" CLUSTER_TYPE_IN = "in" CLUSTER_TYPE_OUT = "out" -COMPONENTS = ( +PLATFORMS = ( BINARY_SENSOR, CLIMATE, COVER, @@ -174,6 +173,9 @@ MFG_CLUSTER_ID_START = 0xFC00 POWER_MAINS_POWERED = "Mains" POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" +PRESET_SCHEDULE = "schedule" +PRESET_COMPLEX = "complex" + class RadioType(enum.Enum): """Possible options for radio type.""" @@ -204,7 +206,7 @@ class RadioType(enum.Enum): ) @classmethod - def list(cls) -> List[str]: + def list(cls) -> list[str]: """Return a list of descriptions.""" return [e.description for e in RadioType] diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index c416548dbe9..c3eec07e980 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -1,5 +1,7 @@ """Decorators for ZHA core registries.""" -from typing import Callable, TypeVar, Union +from __future__ import annotations + +from typing import Callable, TypeVar CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name @@ -8,7 +10,7 @@ class DictRegistry(dict): """Dict Registry of items.""" def register( - self, name: Union[int, str], item: Union[str, CALLABLE_T] = None + self, name: int | str, item: str | CALLABLE_T = None ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Return decorator to register item with a specific name.""" @@ -26,7 +28,7 @@ class DictRegistry(dict): class SetRegistry(set): """Set Registry of items.""" - def register(self, name: Union[int, str]) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: int | str) -> Callable[[CALLABLE_T], CALLABLE_T]: """Return decorator to register item with a specific name.""" def decorator(channel: CALLABLE_T) -> CALLABLE_T: diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index cd3b1bd93ce..65605b2f7a3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -1,11 +1,13 @@ """Device for Zigbee Home Automation.""" +from __future__ import annotations + import asyncio from datetime import timedelta from enum import Enum import logging import random import time -from typing import Any, Dict +from typing import Any from zigpy import types import zigpy.exceptions @@ -14,6 +16,7 @@ import zigpy.quirks from zigpy.zcl.clusters.general import Groups import zigpy.zdo.types as zdo_types +from homeassistant.const import ATTR_COMMAND, ATTR_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -28,7 +31,6 @@ from .const import ( ATTR_ATTRIBUTE, ATTR_AVAILABLE, ATTR_CLUSTER_ID, - ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_DEVICE_TYPE, ATTR_ENDPOINT_ID, @@ -40,7 +42,6 @@ from .const import ( ATTR_MANUFACTURER, ATTR_MANUFACTURER_CODE, ATTR_MODEL, - ATTR_NAME, ATTR_NEIGHBORS, ATTR_NODE_DESCRIPTOR, ATTR_NWK, @@ -276,7 +277,7 @@ class ZHADevice(LogMixin): self._available = new_availability @property - def zigbee_signature(self) -> Dict[str, Any]: + def zigbee_signature(self) -> dict[str, Any]: """Get zigbee signature for this device.""" return { ATTR_NODE_DESCRIPTOR: str(self._zigpy_device.node_desc), diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index e071a523321..338796acffe 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -1,8 +1,9 @@ """Device discovery functions for Zigbee Home Automation.""" +from __future__ import annotations from collections import Counter import logging -from typing import Callable, List, Tuple +from typing import Callable from homeassistant import const as ha_const from homeassistant.core import callback @@ -34,10 +35,10 @@ _LOGGER = logging.getLogger(__name__) @callback async def async_add_entities( _async_add_entities: Callable, - entities: List[ - Tuple[ + entities: list[ + tuple[ zha_typing.ZhaEntityType, - Tuple[str, zha_typing.ZhaDeviceType, List[zha_typing.ChannelType]], + tuple[str, zha_typing.ZhaDeviceType, list[zha_typing.ChannelType]], ] ], update_before_add: bool = True, @@ -75,7 +76,7 @@ class ProbeEndpoint: ep_device_type = channel_pool.endpoint.device_type component = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) - if component and component in zha_const.COMPONENTS: + if component and component in zha_const.PLATFORMS: channels = channel_pool.unclaimed_channels() entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity( component, channel_pool.manufacturer, channel_pool.model, channels @@ -122,7 +123,7 @@ class ProbeEndpoint: ep_channels: zha_typing.ChannelPoolType, ) -> None: """Probe specified cluster for specific component.""" - if component is None or component not in zha_const.COMPONENTS: + if component is None or component not in zha_const.PLATFORMS: return channel_list = [channel] unique_id = f"{ep_channels.unique_id}-{channel.cluster.cluster_id}" @@ -235,9 +236,9 @@ class GroupProbe: @staticmethod def determine_entity_domains( hass: HomeAssistantType, group: zha_typing.ZhaGroupType - ) -> List[str]: + ) -> list[str]: """Determine the entity domains for this group.""" - entity_domains: List[str] = [] + entity_domains: list[str] = [] zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] all_domain_occurrences = [] for member in group.members: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index c57c7269723..de65ed6695e 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -1,4 +1,5 @@ """Virtual gateway for Zigbee Home Automation.""" +from __future__ import annotations import asyncio import collections @@ -9,7 +10,6 @@ import logging import os import time import traceback -from typing import List, Optional from serial import SerialException from zigpy.config import CONF_DEVICE @@ -347,7 +347,8 @@ class ZHAGateway: remove_tasks = [] for entity_ref in entity_refs: remove_tasks.append(entity_ref.remove_future) - await asyncio.wait(remove_tasks) + if remove_tasks: + await asyncio.wait(remove_tasks) reg_device = self.ha_device_registry.async_get(device.device_id) if reg_device is not None: self.ha_device_registry.async_remove_device(reg_device.id) @@ -377,12 +378,12 @@ class ZHAGateway: """Return ZHADevice for given ieee.""" return self._devices.get(ieee) - def get_group(self, group_id: str) -> Optional[ZhaGroupType]: + def get_group(self, group_id: str) -> ZhaGroupType | None: """Return Group for given group id.""" return self.groups.get(group_id) @callback - def async_get_group_by_name(self, group_name: str) -> Optional[ZhaGroupType]: + def async_get_group_by_name(self, group_name: str) -> ZhaGroupType | None: """Get ZHA group by name.""" for group in self.groups.values(): if group.name == group_name: @@ -472,24 +473,29 @@ class ZHAGateway: ) @callback - def async_enable_debug_mode(self): + def async_enable_debug_mode(self, filterer=None): """Enable debug mode for ZHA.""" self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() async_set_logger_levels(DEBUG_LEVELS) self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() + if filterer: + self._log_relay_handler.addFilter(filterer) + for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).addHandler(self._log_relay_handler) self.debug_enabled = True @callback - def async_disable_debug_mode(self): + def async_disable_debug_mode(self, filterer=None): """Disable debug mode for ZHA.""" async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).removeHandler(self._log_relay_handler) + if filterer: + self._log_relay_handler.removeFilter(filterer) self.debug_enabled = False @callback @@ -619,7 +625,7 @@ class ZHAGateway: zha_device.update_available(True) async def async_create_zigpy_group( - self, name: str, members: List[GroupMember] + self, name: str, members: list[GroupMember] ) -> ZhaGroupType: """Create a new Zigpy Zigbee group.""" # we start with two to fill any gaps from a user removing existing groups @@ -727,9 +733,8 @@ class LogRelayHandler(logging.Handler): def emit(self, record): """Relay log message via dispatcher.""" stack = [] - if record.levelno >= logging.WARN: - if not record.exc_info: - stack = [f for f, _, _, _ in traceback.extract_stack()] + if record.levelno >= logging.WARN and not record.exc_info: + stack = [f for f, _, _, _ in traceback.extract_stack()] entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) async_dispatcher_send( diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 59277a394b3..beaebbe8767 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -1,8 +1,10 @@ """Group for Zigbee Home Automation.""" +from __future__ import annotations + import asyncio import collections import logging -from typing import Any, Dict, List +from typing import Any import zigpy.exceptions @@ -58,16 +60,16 @@ class ZHAGroupMember(LogMixin): return self._zha_device @property - def member_info(self) -> Dict[str, Any]: + def member_info(self) -> dict[str, Any]: """Get ZHA group info.""" - member_info: Dict[str, Any] = {} + member_info: dict[str, Any] = {} member_info["endpoint_id"] = self.endpoint_id member_info["device"] = self.device.zha_device_info member_info["entities"] = self.associated_entities return member_info @property - def associated_entities(self) -> List[GroupEntityReference]: + def associated_entities(self) -> list[GroupEntityReference]: """Return the list of entities that were derived from this endpoint.""" ha_entity_registry = self.device.gateway.ha_entity_registry zha_device_registry = self.device.gateway.device_registry @@ -136,7 +138,7 @@ class ZHAGroup(LogMixin): return self._zigpy_group.endpoint @property - def members(self) -> List[ZHAGroupMember]: + def members(self) -> list[ZHAGroupMember]: """Return the ZHA devices that are members of this group.""" return [ ZHAGroupMember( @@ -146,7 +148,7 @@ class ZHAGroup(LogMixin): if member_ieee in self._zha_gateway.devices ] - async def async_add_members(self, members: List[GroupMember]) -> None: + async def async_add_members(self, members: list[GroupMember]) -> None: """Add members to this group.""" if len(members) > 1: tasks = [] @@ -162,7 +164,7 @@ class ZHAGroup(LogMixin): members[0].ieee ].async_add_endpoint_to_group(members[0].endpoint_id, self.group_id) - async def async_remove_members(self, members: List[GroupMember]) -> None: + async def async_remove_members(self, members: list[GroupMember]) -> None: """Remove members from this group.""" if len(members) > 1: tasks = [] @@ -181,18 +183,18 @@ class ZHAGroup(LogMixin): ].async_remove_endpoint_from_group(members[0].endpoint_id, self.group_id) @property - def member_entity_ids(self) -> List[str]: + def member_entity_ids(self) -> list[str]: """Return the ZHA entity ids for all entities for the members of this group.""" - all_entity_ids: List[str] = [] + all_entity_ids: list[str] = [] for member in self.members: entity_references = member.associated_entities for entity_reference in entity_references: all_entity_ids.append(entity_reference["entity_id"]) return all_entity_ids - def get_domain_entity_ids(self, domain) -> List[str]: + def get_domain_entity_ids(self, domain) -> list[str]: """Return entity ids from the entity domain for this group.""" - domain_entity_ids: List[str] = [] + domain_entity_ids: list[str] = [] for member in self.members: if member.device.is_coordinator: continue @@ -207,9 +209,9 @@ class ZHAGroup(LogMixin): return domain_entity_ids @property - def group_info(self) -> Dict[str, Any]: + def group_info(self) -> dict[str, Any]: """Get ZHA group info.""" - group_info: Dict[str, Any] = {} + group_info: dict[str, Any] = {} group_info["group_id"] = self.group_id group_info["name"] = self.name group_info["members"] = [member.member_info for member in self.members] diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 47911fc1078..cf3d040f020 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -4,6 +4,7 @@ Helpers for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ +from __future__ import annotations import asyncio import binascii @@ -13,7 +14,7 @@ import itertools import logging from random import uniform import re -from typing import Any, Callable, Iterator, List, Optional, Tuple +from typing import Any, Callable, Iterator import voluptuous as vol import zigpy.exceptions @@ -67,7 +68,7 @@ async def safe_read( async def get_matched_clusters( source_zha_device: ZhaDeviceType, target_zha_device: ZhaDeviceType -) -> List[BindingPair]: +) -> list[BindingPair]: """Get matched input/output cluster pairs for 2 devices.""" source_clusters = source_zha_device.async_get_std_clusters() target_clusters = target_zha_device.async_get_std_clusters() @@ -131,7 +132,7 @@ async def async_get_zha_device(hass, device_id): return zha_gateway.devices[ieee] -def find_state_attributes(states: List[State], key: str) -> Iterator[Any]: +def find_state_attributes(states: list[State], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: value = state.attributes.get(key) @@ -150,9 +151,9 @@ def mean_tuple(*args): def reduce_attribute( - states: List[State], + states: list[State], key: str, - default: Optional[Any] = None, + default: Any | None = None, reduce: Callable[..., Any] = mean_int, ) -> Any: """Find the first attribute matching key from states. @@ -280,7 +281,7 @@ QR_CODES = ( ) -def qr_to_install_code(qr_code: str) -> Tuple[zigpy.types.EUI64, bytes]: +def qr_to_install_code(qr_code: str) -> tuple[zigpy.types.EUI64, bytes]: """Try to parse the QR code. if successful, return a tuple of a EUI64 address and install code. diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 4dcccc98c05..2f9ed57745a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -1,6 +1,8 @@ """Mapping registries for Zigbee Home Automation.""" +from __future__ import annotations + import collections -from typing import Callable, Dict, List, Set, Tuple, Union +from typing import Callable, Dict import attr import zigpy.profiles.zha @@ -68,6 +70,8 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR, zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR, zcl.clusters.hvac.Fan.cluster_id: FAN, + zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: SENSOR, + zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: SENSOR, zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR, zcl.clusters.measurement.OccupancySensing.cluster_id: BINARY_SENSOR, zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR, @@ -132,19 +136,19 @@ def set_or_callable(value): class MatchRule: """Match a ZHA Entity to a channel name or generic id.""" - channel_names: Union[Callable, Set[str], str] = attr.ib( + channel_names: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) - generic_ids: Union[Callable, Set[str], str] = attr.ib( + generic_ids: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) - manufacturers: Union[Callable, Set[str], str] = attr.ib( + manufacturers: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) - models: Union[Callable, Set[str], str] = attr.ib( + models: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) - aux_channels: Union[Callable, Set[str], str] = attr.ib( + aux_channels: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) @@ -174,7 +178,7 @@ class MatchRule: weight += 1 * len(self.aux_channels) return weight - def claim_channels(self, channel_pool: List[ChannelType]) -> List[ChannelType]: + def claim_channels(self, channel_pool: list[ChannelType]) -> list[ChannelType]: """Return a list of channels this rule matches + aux channels.""" claimed = [] if isinstance(self.channel_names, frozenset): @@ -187,15 +191,15 @@ class MatchRule: claimed.extend([ch for ch in channel_pool if ch.name in self.aux_channels]) return claimed - def strict_matched(self, manufacturer: str, model: str, channels: List) -> bool: + def strict_matched(self, manufacturer: str, model: str, channels: list) -> bool: """Return True if this device matches the criteria.""" return all(self._matched(manufacturer, model, channels)) - def loose_matched(self, manufacturer: str, model: str, channels: List) -> bool: + def loose_matched(self, manufacturer: str, model: str, channels: list) -> bool: """Return True if this device matches the criteria.""" return any(self._matched(manufacturer, model, channels)) - def _matched(self, manufacturer: str, model: str, channels: List) -> list: + def _matched(self, manufacturer: str, model: str, channels: list) -> list: """Return a list of field matches.""" if not any(attr.asdict(self).values()): return [False] @@ -243,9 +247,9 @@ class ZHAEntityRegistry: component: str, manufacturer: str, model: str, - channels: List[ChannelType], + channels: list[ChannelType], default: CALLABLE_T = None, - ) -> Tuple[CALLABLE_T, List[ChannelType]]: + ) -> tuple[CALLABLE_T, list[ChannelType]]: """Match a ZHA Channels to a ZHA Entity class.""" matches = self._strict_registry[component] for match in sorted(matches, key=lambda x: x.weight, reverse=True): @@ -262,11 +266,11 @@ class ZHAEntityRegistry: def strict_match( self, component: str, - channel_names: Union[Callable, Set[str], str] = None, - generic_ids: Union[Callable, Set[str], str] = None, - manufacturers: Union[Callable, Set[str], str] = None, - models: Union[Callable, Set[str], str] = None, - aux_channels: Union[Callable, Set[str], str] = None, + channel_names: Callable | set[str] | str = None, + generic_ids: Callable | set[str] | str = None, + manufacturers: Callable | set[str] | str = None, + models: Callable | set[str] | str = None, + aux_channels: Callable | set[str] | str = None, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a strict match rule.""" @@ -287,11 +291,11 @@ class ZHAEntityRegistry: def loose_match( self, component: str, - channel_names: Union[Callable, Set[str], str] = None, - generic_ids: Union[Callable, Set[str], str] = None, - manufacturers: Union[Callable, Set[str], str] = None, - models: Union[Callable, Set[str], str] = None, - aux_channels: Union[Callable, Set[str], str] = None, + channel_names: Callable | set[str] | str = None, + generic_ids: Callable | set[str] | str = None, + manufacturers: Callable | set[str] | str = None, + models: Callable | set[str] | str = None, + aux_channels: Callable | set[str] | str = None, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a loose match rule.""" diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 051fcaa2925..9381c529187 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -1,9 +1,10 @@ """Data storage helper for ZHA.""" -# pylint: disable=unused-import +from __future__ import annotations + from collections import OrderedDict import datetime import time -from typing import MutableMapping, Optional, cast +from typing import MutableMapping, cast import attr @@ -25,9 +26,9 @@ TOMBSTONE_LIFETIME = datetime.timedelta(days=60).total_seconds() class ZhaDeviceEntry: """Zha Device storage Entry.""" - name: Optional[str] = attr.ib(default=None) - ieee: Optional[str] = attr.ib(default=None) - last_seen: Optional[float] = attr.ib(default=None) + name: str | None = attr.ib(default=None) + ieee: str | None = attr.ib(default=None) + last_seen: float | None = attr.ib(default=None) class ZhaStorage: @@ -92,7 +93,7 @@ class ZhaStorage: """Load the registry of zha device entries.""" data = await self._store.async_load() - devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict() + devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() if data is not None: for device in data["devices"]: diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 45114c677af..5530cd3e3f5 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -1,8 +1,9 @@ """Support for ZHA covers.""" +from __future__ import annotations + import asyncio import functools import logging -from typing import List, Optional from zigpy.zcl.foundation import Status @@ -180,7 +181,7 @@ class Shade(ZhaEntity, CoverEntity): self, unique_id: str, zha_device: ZhaDeviceType, - channels: List[ChannelType], + channels: list[ChannelType], **kwargs, ): """Initialize the ZHA light.""" @@ -199,12 +200,12 @@ class Shade(ZhaEntity, CoverEntity): return self._position @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_SHADE @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return True if shade is closed.""" if self._is_open is None: return None @@ -289,7 +290,7 @@ class KeenVent(Shade): """Keen vent cover.""" @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_DAMPER @@ -301,7 +302,7 @@ class KeenVent(Shade): self._on_off_channel.on(), ] results = await asyncio.gather(*tasks, return_exceptions=True) - if any([isinstance(result, Exception) for result in results]): + if any(isinstance(result, Exception) for result in results): self.debug("couldn't open cover") return diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 46363939190..9d419b16435 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -1,5 +1,5 @@ """Provides device actions for ZHA devices.""" -from typing import List +from __future__ import annotations import voluptuous as vol @@ -54,7 +54,7 @@ async def async_call_action_from_config( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: """List device actions.""" try: zha_device = await async_get_zha_device(hass, device_id) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 53191789eba..ffb37e33b0f 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -83,7 +83,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): @callback def async_battery_percentage_remaining_updated(self, attr_id, attr_name, value): """Handle tracking.""" - if not attr_name == "battery_percentage_remaining": + if attr_name != "battery_percentage_remaining": return self.debug("battery_percentage_remaining updated: %s", value) self._connected = True diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index db30e9e178c..445151899ee 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -1,10 +1,12 @@ """Entity for Zigbee Home Automation.""" +from __future__ import annotations import asyncio import functools import logging -from typing import Any, Awaitable, Dict, List, Optional +from typing import Any, Awaitable +from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -18,7 +20,6 @@ from homeassistant.helpers.restore_state import RestoreEntity from .core.const import ( ATTR_MANUFACTURER, ATTR_MODEL, - ATTR_NAME, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, @@ -32,6 +33,7 @@ from .core.typing import CALLABLE_T, ChannelType, ZhaDeviceType _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" +UPDATE_GROUP_FROM_CHILD_DELAY = 0.2 class BaseZhaEntity(LogMixin, entity.Entity): @@ -44,9 +46,9 @@ class BaseZhaEntity(LogMixin, entity.Entity): self._should_poll: bool = False self._unique_id: str = unique_id self._state: Any = None - self._device_state_attributes: Dict[str, Any] = {} + self._extra_state_attributes: dict[str, Any] = {} self._zha_device: ZhaDeviceType = zha_device - self._unsubs: List[CALLABLE_T] = [] + self._unsubs: list[CALLABLE_T] = [] self.remove_future: Awaitable[None] = None @property @@ -65,9 +67,9 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._zha_device @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" - return self._device_state_attributes + return self._extra_state_attributes @property def force_update(self) -> bool: @@ -80,7 +82,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._should_poll @property - def device_info(self) -> Dict[str, Any]: + def device_info(self) -> dict[str, Any]: """Return a device description for device registry.""" zha_device_info = self._zha_device.device_info ieee = zha_device_info["ieee"] @@ -101,7 +103,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): @callback def async_update_state_attribute(self, key: str, value: Any) -> None: """Update a single device state attribute.""" - self._device_state_attributes.update({key: value}) + self._extra_state_attributes.update({key: value}) self.async_write_ha_state() @callback @@ -142,7 +144,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): self, unique_id: str, zha_device: ZhaDeviceType, - channels: List[ChannelType], + channels: list[ChannelType], **kwargs, ): """Init ZHA entity.""" @@ -151,7 +153,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): ch_names = [ch.cluster.ep_attribute for ch in channels] ch_names = ", ".join(sorted(ch_names)) self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" - self.cluster_channels: Dict[str, ChannelType] = {} + self.cluster_channels: dict[str, ChannelType] = {} for channel in channels: self.cluster_channels[channel.name] = channel @@ -216,7 +218,7 @@ class ZhaGroupEntity(BaseZhaEntity): """A base class for ZHA group entities.""" def __init__( - self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a light group.""" super().__init__(unique_id, zha_device, **kwargs) @@ -224,8 +226,8 @@ class ZhaGroupEntity(BaseZhaEntity): self._group = zha_device.gateway.groups.get(group_id) self._name = f"{self._group.name}_zha_group_0x{group_id:04x}" self._group_id: int = group_id - self._entity_ids: List[str] = entity_ids - self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None + self._entity_ids: list[str] = entity_ids + self._async_unsub_state_changed: CALLBACK_TYPE | None = None self._handled_group_membership = False @property @@ -267,7 +269,11 @@ class ZhaGroupEntity(BaseZhaEntity): @callback def async_state_changed_listener(self, event: Event): """Handle child updates.""" - self.async_schedule_update_ha_state(True) + # Delay to ensure that we get updates from all members before updating the group + self.hass.loop.call_later( + UPDATE_GROUP_FROM_CHILD_DELAY, + lambda: self.async_schedule_update_ha_state(True), + ) async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index ed041f8c6c0..3c50261b565 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,8 +1,9 @@ """Fans on Zigbee Home Automation networks.""" +from __future__ import annotations + from abc import abstractmethod import functools import math -from typing import List, Optional from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.hvac as hvac @@ -77,7 +78,7 @@ class BaseFan(FanEntity): """Base representation of a ZHA fan.""" @property - def preset_modes(self) -> List[str]: + def preset_modes(self) -> list[str]: """Return the available preset modes.""" return PRESET_MODES @@ -103,7 +104,7 @@ class BaseFan(FanEntity): """Turn the entity off.""" await self.async_set_percentage(0) - async def async_set_percentage(self, percentage: Optional[int]) -> None: + async def async_set_percentage(self, percentage: int | None) -> None: """Set the speed percenage of the fan.""" fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) await self._async_set_fan_mode(fan_mode) @@ -142,7 +143,7 @@ class ZhaFan(BaseFan, ZhaEntity): ) @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the current speed percentage.""" if ( self._fan_channel.fan_mode is None @@ -154,7 +155,7 @@ class ZhaFan(BaseFan, ZhaEntity): return ranged_value_to_percentage(SPEED_RANGE, self._fan_channel.fan_mode) @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode.""" return PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode) @@ -174,7 +175,7 @@ class FanGroup(BaseFan, ZhaGroupEntity): """Representation of a fan group.""" def __init__( - self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a fan group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -185,12 +186,12 @@ class FanGroup(BaseFan, ZhaGroupEntity): self._preset_mode = None @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the current speed percentage.""" return self._percentage @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode.""" return self._preset_mode @@ -205,11 +206,11 @@ class FanGroup(BaseFan, ZhaGroupEntity): async def async_update(self): """Attempt to retrieve on off state from the fan.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: List[State] = list(filter(None, all_states)) - percentage_states: List[State] = [ + states: list[State] = list(filter(None, all_states)) + percentage_states: list[State] = [ state for state in states if state.attributes.get(ATTR_PERCENTAGE) ] - preset_mode_states: List[State] = [ + preset_mode_states: list[State] = [ state for state in states if state.attributes.get(ATTR_PRESET_MODE) ] self._available = any(state.state != STATE_UNAVAILABLE for state in states) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 32b8a064054..72807458d26 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,4 +1,6 @@ """Lights on Zigbee Home Automation networks.""" +from __future__ import annotations + from collections import Counter from datetime import timedelta import enum @@ -6,7 +8,7 @@ import functools import itertools import logging import random -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff from zigpy.zcl.clusters.lighting import Color @@ -65,6 +67,8 @@ CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 +DEFAULT_TRANSITION = 1 + UPDATE_COLORLOOP_ACTION = 0x1 UPDATE_COLORLOOP_DIRECTION = 0x2 UPDATE_COLORLOOP_TIME = 0x4 @@ -114,19 +118,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" + _FORCE_ON = False + def __init__(self, *args, **kwargs): """Initialize the light.""" super().__init__(*args, **kwargs) self._available: bool = False - self._brightness: Optional[int] = None - self._off_brightness: Optional[int] = None - self._hs_color: Optional[Tuple[float, float]] = None - self._color_temp: Optional[int] = None - self._min_mireds: Optional[int] = 153 - self._max_mireds: Optional[int] = 500 - self._white_value: Optional[int] = None - self._effect_list: Optional[List[str]] = None - self._effect: Optional[str] = None + self._brightness: int | None = None + self._off_brightness: int | None = None + self._hs_color: tuple[float, float] | None = None + self._color_temp: int | None = None + self._min_mireds: int | None = 153 + self._max_mireds: int | None = 500 + self._white_value: int | None = None + self._effect_list: list[str] | None = None + self._effect: str | None = None self._supported_features: int = 0 self._state: bool = False self._on_off_channel = None @@ -135,7 +141,7 @@ class BaseLight(LogMixin, light.LightEntity): self._identify_channel = None @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return state attributes.""" attributes = {"off_brightness": self._off_brightness} return attributes @@ -201,7 +207,7 @@ class BaseLight(LogMixin, light.LightEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition else 1 + duration = transition * 10 if transition else DEFAULT_TRANSITION brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) @@ -228,7 +234,7 @@ class BaseLight(LogMixin, light.LightEntity): if level: self._brightness = level - if brightness is None or brightness: + if brightness is None or (self._FORCE_ON and brightness): # since some lights don't always turn on with move_to_level_with_on_off, # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() @@ -344,8 +350,8 @@ class Light(BaseLight, ZhaEntity): self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._identify_channel = self.zha_device.channels.identify_ch if self._color_channel: - self._min_mireds: Optional[int] = self._color_channel.min_mireds - self._max_mireds: Optional[int] = self._color_channel.max_mireds + self._min_mireds: int | None = self._color_channel.min_mireds + self._max_mireds: int | None = self._color_channel.max_mireds self._cancel_refresh_handle = None effect_list = [] @@ -512,12 +518,23 @@ class HueLight(Light): _REFRESH_INTERVAL = (3, 5) +@STRICT_MATCH( + channel_names=CHANNEL_ON_OFF, + aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, + manufacturers="Jasco", +) +class ForceOnLight(Light): + """Representation of a light which does not respect move_to_level_with_on_off.""" + + _FORCE_ON = True + + @GROUP_MATCH() class LightGroup(BaseLight, ZhaGroupEntity): """Representation of a light group.""" def __init__( - self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -554,7 +571,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: List[State] = list(filter(None, all_states)) + states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._state = len(on_states) > 0 diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 5cc7d7c56f6..5684b22db6a 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,6 +1,7 @@ """Locks on Zigbee Home Automation networks.""" import functools +import voluptuous as vol from zigpy.zcl.foundation import Status from homeassistant.components.lock import ( @@ -10,6 +11,7 @@ from homeassistant.components.lock import ( LockEntity, ) from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core import discovery @@ -29,6 +31,11 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) +SERVICE_SET_LOCK_USER_CODE = "set_lock_user_code" +SERVICE_ENABLE_LOCK_USER_CODE = "enable_lock_user_code" +SERVICE_DISABLE_LOCK_USER_CODE = "disable_lock_user_code" +SERVICE_CLEAR_LOCK_USER_CODE = "clear_lock_user_code" + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation Door Lock from config entry.""" @@ -43,6 +50,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + platform = entity_platform.current_platform.get() + assert platform + + platform.async_register_entity_service( # type: ignore + SERVICE_SET_LOCK_USER_CODE, + { + vol.Required("code_slot"): vol.Coerce(int), + vol.Required("user_code"): cv.string, + }, + "async_set_lock_user_code", + ) + + platform.async_register_entity_service( # type: ignore + SERVICE_ENABLE_LOCK_USER_CODE, + { + vol.Required("code_slot"): vol.Coerce(int), + }, + "async_enable_lock_user_code", + ) + + platform.async_register_entity_service( # type: ignore + SERVICE_DISABLE_LOCK_USER_CODE, + { + vol.Required("code_slot"): vol.Coerce(int), + }, + "async_disable_lock_user_code", + ) + + platform.async_register_entity_service( # type: ignore + SERVICE_CLEAR_LOCK_USER_CODE, + { + vol.Required("code_slot"): vol.Coerce(int), + }, + "async_clear_lock_user_code", + ) + @STRICT_MATCH(channel_names=CHANNEL_DOORLOCK) class ZhaDoorLock(ZhaEntity, LockEntity): @@ -73,7 +116,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): return self._state == STATE_LOCKED @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return state attributes.""" return self.state_attributes @@ -116,3 +159,27 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def refresh(self, time): """Call async_get_state at an interval.""" await self.async_get_state(from_cache=False) + + async def async_set_lock_user_code(self, code_slot: int, user_code: str) -> None: + """Set the user_code to index X on the lock.""" + if self._doorlock_channel: + await self._doorlock_channel.async_set_user_code(code_slot, user_code) + self.debug("User code at slot %s set", code_slot) + + async def async_enable_lock_user_code(self, code_slot: int) -> None: + """Enable user_code at index X on the lock.""" + if self._doorlock_channel: + await self._doorlock_channel.async_enable_user_code(code_slot) + self.debug("User code at slot %s enabled", code_slot) + + async def async_disable_lock_user_code(self, code_slot: int) -> None: + """Disable user_code at index X on the lock.""" + if self._doorlock_channel: + await self._doorlock_channel.async_disable_user_code(code_slot) + self.debug("User code at slot %s disabled", code_slot) + + async def async_clear_lock_user_code(self, code_slot: int) -> None: + """Clear the user_code at index X on the lock.""" + if self._doorlock_channel: + await self._doorlock_channel.async_clear_user_code(code_slot) + self.debug("User code at slot %s cleared", code_slot) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7d367c3dc00..5825bdcda0f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,16 +4,18 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.22.0", + "bellows==0.23.1", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.54", + "zha-quirks==0.0.55", "zigpy-cc==0.5.2", - "zigpy-deconz==0.11.1", - "zigpy==0.32.0", + "zigpy-deconz==0.12.0", + "zigpy==0.33.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.4.0" ], - "codeowners": ["@dmulcahey", "@adminiuga"] + "codeowners": ["@dmulcahey", "@adminiuga"], + "zeroconf": [{ "type": "_esphomelib._tcp.local.", "name": "tube*" }], + "after_dependencies": ["zeroconf"] } diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b02b3a549be..41dce816e86 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,19 +1,25 @@ """Sensors on Zigbee Home Automation networks.""" +from __future__ import annotations + import functools import numbers -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DOMAIN, + SensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -84,21 +90,21 @@ async def async_setup_entry( hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class Sensor(ZhaEntity): +class Sensor(ZhaEntity, SensorEntity): """Base ZHA sensor.""" - SENSOR_ATTR: Optional[Union[int, str]] = None + SENSOR_ATTR: int | str | None = None _decimals: int = 1 - _device_class: Optional[str] = None + _device_class: str | None = None _divisor: int = 1 _multiplier: int = 1 - _unit: Optional[str] = None + _unit: str | None = None def __init__( self, unique_id: str, zha_device: ZhaDeviceType, - channels: List[ChannelType], + channels: list[ChannelType], **kwargs, ): """Init this sensor.""" @@ -118,7 +124,7 @@ class Sensor(ZhaEntity): return self._device_class @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" return self._unit @@ -136,7 +142,7 @@ class Sensor(ZhaEntity): """Handle state update from channel.""" self.async_write_ha_state() - def formatter(self, value: int) -> Union[int, float]: + def formatter(self, value: int) -> int | float: """Numeric pass-through formatter.""" if self._decimals > 0: return round( @@ -175,7 +181,7 @@ class Battery(Sensor): return value @property - def device_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attrs for battery sensors.""" state_attrs = {} battery_size = self._channel.cluster.get("battery_size") @@ -186,7 +192,9 @@ class Battery(Sensor): state_attrs["battery_quantity"] = battery_quantity battery_voltage = self._channel.cluster.get("battery_voltage") if battery_voltage is not None: - state_attrs["battery_voltage"] = round(battery_voltage / 10, 1) + v_10mv = round(battery_voltage / 10, 2) + v_100mv = round(battery_voltage / 10, 1) + state_attrs["battery_voltage"] = v_100mv if v_100mv == v_10mv else v_10mv return state_attrs @@ -203,7 +211,7 @@ class ElectricalMeasurement(Sensor): """Return True if HA needs to poll for state changes.""" return True - def formatter(self, value: int) -> Union[int, float]: + def formatter(self, value: int) -> int | float: """Return 'normalized' value.""" value = value * self._channel.multiplier / self._channel.divisor if value < 100 and self._channel.divisor > 1: @@ -249,7 +257,7 @@ class SmartEnergyMetering(Sensor): SENSOR_ATTR = "instantaneous_demand" _device_class = DEVICE_CLASS_POWER - def formatter(self, value: int) -> Union[int, float]: + def formatter(self, value: int) -> int | float: """Pass through channel formatter.""" return self._channel.formatter_function(value) @@ -277,3 +285,25 @@ class Temperature(Sensor): _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 _unit = TEMP_CELSIUS + + +@STRICT_MATCH(channel_names="carbon_dioxide_concentration") +class CarbonDioxideConcentration(Sensor): + """Carbon Dioxide Concentration sensor.""" + + SENSOR_ATTR = "measured_value" + _device_class = DEVICE_CLASS_CO2 + _decimals = 0 + _multiplier = 1e6 + _unit = CONCENTRATION_PARTS_PER_MILLION + + +@STRICT_MATCH(channel_names="carbon_monoxide_concentration") +class CarbonMonoxideConcentration(Sensor): + """Carbon Monoxide Concentration sensor.""" + + SENSOR_ATTR = "measured_value" + _device_class = DEVICE_CLASS_CO + _decimals = 0 + _multiplier = 1e6 + _unit = CONCENTRATION_PARTS_PER_MILLION diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 74793d6000f..e756edbc48b 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -163,3 +163,74 @@ warning_device_warn: description: >- Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. example: 2 + +clear_lock_user_code: + name: Clear lock user + description: Clear a user code from a lock + target: + entity: + domain: lock + integration: zha + fields: + code_slot: + name: Code slot + description: Code slot to clear code from + required: true + example: 1 + selector: + text: + +enable_lock_user_code: + name: Enable lock user + description: Enable a user code on a lock + target: + entity: + domain: lock + integration: zha + fields: + code_slot: + name: Code slot + description: Code slot to enable + required: true + example: 1 + selector: + text: + +disable_lock_user_code: + name: Disable lock user + description: Disable a user code on a lock + target: + entity: + domain: lock + integration: zha + fields: + code_slot: + name: Code slot + description: Code slot to disable + required: true + example: 1 + selector: + text: + +set_lock_user_code: + name: Set lock user code + description: Set a user code on a lock + target: + entity: + domain: lock + integration: zha + fields: + code_slot: + name: Code slot + description: Code slot to set the code in + required: true + example: 1 + selector: + text: + user_code: + name: Code + description: Code to set + required: true + example: 1234 + selector: + text: diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 93b5cd7ccf5..550fad3c2c5 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "ZHA: {name}", "step": { "user": { "title": "ZHA", @@ -21,7 +22,9 @@ } } }, - "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 60db9ec0a08..75254f631b9 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,6 +1,8 @@ """Switches on Zigbee Home Automation networks.""" +from __future__ import annotations + import functools -from typing import Any, List +from typing import Any from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status @@ -113,7 +115,7 @@ class SwitchGroup(BaseSwitch, ZhaGroupEntity): """Representation of a switch group.""" def __init__( - self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a switch group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -124,7 +126,7 @@ class SwitchGroup(BaseSwitch, ZhaGroupEntity): async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: List[State] = list(filter(None, all_states)) + states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._state = len(on_states) > 0 diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 7fb36f11532..20eee443960 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index cedf56d73c4..2422941312b 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, + "flow_title": "ZHA: {name}", "step": { "port_config": { "data": { diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index d9c78171d94..d3ed2ddfce4 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 4926476a5c6..d03300f9971 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 935663ed9e4..844f2dd7191 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Csak egyetlen ZHA konfigur\u00e1ci\u00f3 megengedett." + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { - "cannot_connect": "Nem lehet csatlakozni a ZHA eszk\u00f6zh\u00f6z." + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "port_config": { @@ -17,5 +17,10 @@ "title": "ZHA" } } + }, + "device_automation": { + "trigger_type": { + "device_offline": "Eszk\u00f6z offline" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json new file mode 100644 index 00000000000..5baf04e1314 --- /dev/null +++ b/homeassistant/components/zha/translations/id.json @@ -0,0 +1,91 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "pick_radio": { + "data": { + "radio_type": "Jenis Radio" + }, + "description": "Pilih jenis radio Zigbee Anda", + "title": "Jenis Radio" + }, + "port_config": { + "data": { + "baudrate": "kecepatan port", + "flow_control": "kontrol data flow", + "path": "Jalur perangkat serial" + }, + "description": "Masukkan pengaturan khusus port", + "title": "Setelan" + }, + "user": { + "data": { + "path": "Jalur Perangkat Serial" + }, + "description": "Pilih port serial untuk radio Zigbee", + "title": "ZHA" + } + } + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Peringatkan" + }, + "trigger_subtype": { + "both_buttons": "Kedua tombol", + "button_1": "Tombol pertama", + "button_2": "Tombol kedua", + "button_3": "Tombol ketiga", + "button_4": "Tombol keempat", + "button_5": "Tombol kelima", + "button_6": "Tombol keenam", + "close": "Tutup", + "dim_down": "Redupkan", + "dim_up": "Terangkan", + "face_1": "dengan wajah 1 diaktifkan", + "face_2": "dengan wajah 2 diaktifkan", + "face_3": "dengan wajah 3 diaktifkan", + "face_4": "dengan wajah 4 diaktifkan", + "face_5": "dengan wajah 5 diaktifkan", + "face_6": "dengan wajah 6 diaktifkan", + "face_any": "Dengan wajah apa pun/yang ditentukan diaktifkan", + "left": "Kiri", + "open": "Buka", + "right": "Kanan", + "turn_off": "Matikan", + "turn_on": "Nyalakan" + }, + "trigger_type": { + "device_dropped": "Perangkat dijatuhkan", + "device_flipped": "Perangkat dibalik \"{subtype}\"", + "device_knocked": "Perangkat diketuk \"{subtype}\"", + "device_offline": "Perangkat offline", + "device_rotated": "Perangkat diputar \"{subtype}\"", + "device_shaken": "Perangkat diguncangkan", + "device_slid": "Perangkat diluncurkan \"{subtype}\"", + "device_tilted": "Perangkat dimiringkan", + "remote_button_alt_double_press": "Tombol \"{subtype}\" diklik dua kali (Mode alternatif)", + "remote_button_alt_long_press": "Tombol \"{subtype}\" terus ditekan (Mode alternatif)", + "remote_button_alt_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama (Mode alternatif)", + "remote_button_alt_quadruple_press": "Tombol \"{subtype}\" diklik empat kali (Mode alternatif)", + "remote_button_alt_quintuple_press": "Tombol \"{subtype}\" diklik lima kali (Mode alternatif)", + "remote_button_alt_short_press": "Tombol \"{subtype}\" ditekan (Mode alternatif)", + "remote_button_alt_short_release": "Tombol \"{subtype}\" dilepaskan (Mode alternatif)", + "remote_button_alt_triple_press": "Tombol \"{subtype}\" diklik tiga kali (Mode alternatif)", + "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali", + "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan", + "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali", + "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali", + "remote_button_short_press": "Tombol \"{subtype}\" ditekan", + "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", + "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 066a45feab1..e97828d8e2a 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json index 639cc84d86f..4ec9790b357 100644 --- a/homeassistant/components/zha/translations/ko.json +++ b/homeassistant/components/zha/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" @@ -34,8 +34,8 @@ }, "device_automation": { "action_type": { - "squawk": "\ube44\uc0c1", - "warn": "\uacbd\uace0" + "squawk": "\uc2a4\ucffc\ud06c \ud558\uae30", + "warn": "\uacbd\uace0\ud558\uae30" }, "trigger_subtype": { "both_buttons": "\ub450 \uac1c", @@ -63,28 +63,29 @@ }, "trigger_type": { "device_dropped": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc84c\uc744 \ub54c", - "device_flipped": "\"{subtype}\" \uae30\uae30\uac00 \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", - "device_knocked": "\"{subtype}\" \uae30\uae30\uac00 \ub450\ub4dc\ub824\uc9c8 \ub54c", - "device_rotated": "\"{subtype}\" \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", - "device_slid": "\"{subtype}\" \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc9c8 \ub54c", - "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc9c8 \ub54c", - "remote_button_alt_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_alt_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_alt_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c (\ub300\uccb4\ubaa8\ub4dc)", - "remote_button_alt_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_alt_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_alt_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_alt_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_alt_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" + "device_flipped": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ub4a4\uc9d1\uc5b4\uc84c\uc744 \ub54c", + "device_knocked": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ub450\ub4dc\ub824\uc84c\uc744 \ub54c", + "device_offline": "\uae30\uae30\uac00 \uc624\ud504\ub77c\uc778\uc774 \ub418\uc5c8\uc744 \ub54c", + "device_rotated": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub418\uc5c8\uc744 \ub54c", + "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub838\uc744 \ub54c", + "device_slid": "\"{subtype}\"(\uc73c)\ub85c \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc84c\uc744 \ub54c", + "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc84c\uc744 \ub54c", + "remote_button_alt_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c (\ub300\uccb4\ubaa8\ub4dc)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub838\uc744 \ub54c", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \ub5bc\uc600\uc744 \ub54c", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub838\uc744 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub838\uc744 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5bc\uc5c8\uc744 \ub54c", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub838\uc744 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 9a7a83e19d3..ddf208c8577 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van ZHA is toegestaan." + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "error": { - "cannot_connect": "Kan geen verbinding maken met ZHA apparaat." + "cannot_connect": "Kan geen verbinding maken" }, "step": { "pick_radio": { @@ -65,6 +65,7 @@ "device_dropped": "Apparaat gevallen", "device_flipped": "Apparaat omgedraaid \"{subtype}\"", "device_knocked": "Apparaat klopte \"{subtype}\"", + "device_offline": "Apparaat offline", "device_rotated": "Apparaat gedraaid \" {subtype} \"", "device_shaken": "Apparaat geschud", "device_slid": "Apparaat geschoven \"{subtype}\"\".", diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 13394e5a293..6ee8d58f50d 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "ZHA: {name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 68247537847..f20c9f7e328 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -95,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def startup(): """Start hub socket after all climate entity is set up.""" nonlocal hub_is_initialized - if not all([device.is_initialized for device in devices]): + if not all(device.is_initialized for device in devices): return if hub_is_initialized: diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index 5113c5c6e18..4c037a7aa02 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -1,5 +1,5 @@ """Support for tracking the zodiac sign.""" -from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorEntity from homeassistant.util.dt import as_local, utcnow from .const import ( @@ -162,7 +162,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([ZodiacSensor()], True) -class ZodiacSensor(Entity): +class ZodiacSensor(SensorEntity): """Representation of a Zodiac sensor.""" def __init__(self): @@ -196,7 +196,7 @@ class ZodiacSensor(Entity): return ZODIAC_ICONS.get(self._state) @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the state attributes.""" return self._attrs diff --git a/homeassistant/components/zodiac/translations/sensor.hu.json b/homeassistant/components/zodiac/translations/sensor.hu.json new file mode 100644 index 00000000000..8897339b74f --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.hu.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "V\u00edz\u00f6nt\u0151", + "aries": "Kos", + "cancer": "R\u00e1k", + "capricorn": "Bak", + "gemini": "Ikrek", + "leo": "Oroszl\u00e1n", + "libra": "M\u00e9rleg", + "pisces": "Halak", + "sagittarius": "Nyilas", + "scorpio": "Skorpi\u00f3", + "taurus": "Bika", + "virgo": "Sz\u0171z" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.id.json b/homeassistant/components/zodiac/translations/sensor.id.json new file mode 100644 index 00000000000..cd671e146ed --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.id.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aquarius", + "aries": "Aries", + "cancer": "Cancer", + "capricorn": "Capricorn", + "gemini": "Gemini", + "leo": "Leo", + "libra": "Libra", + "pisces": "Pisces", + "sagittarius": "Sagittarius", + "scorpio": "Scorpio", + "taurus": "Taurus", + "virgo": "Virgo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.ko.json b/homeassistant/components/zodiac/translations/sensor.ko.json index 0a9fc83cdea..88221b2f34e 100644 --- a/homeassistant/components/zodiac/translations/sensor.ko.json +++ b/homeassistant/components/zodiac/translations/sensor.ko.json @@ -1,18 +1,18 @@ { "state": { "zodiac__sign": { - "aquarius": "\ubb3c\ubcd1 \uc790\ub9ac", - "aries": "\uc591 \uc790\ub9ac", - "cancer": "\uac8c \uc790\ub9ac", - "capricorn": "\uc5fc\uc18c \uc790\ub9ac", - "gemini": "\uc30d\ub465\uc774 \uc790\ub9ac", - "leo": "\uc0ac\uc790 \uc790\ub9ac", - "libra": "\ucc9c\uce6d \uc790\ub9ac", - "pisces": "\ubb3c\uace0\uae30 \uc790\ub9ac", - "sagittarius": "\uad81\uc218 \uc790\ub9ac", - "scorpio": "\uc804\uac08 \uc790\ub9ac", - "taurus": "\ud669\uc18c \uc790\ub9ac", - "virgo": "\ucc98\ub140 \uc790\ub9ac" + "aquarius": "\ubb3c\ubcd1\uc790\ub9ac", + "aries": "\uc591\uc790\ub9ac", + "cancer": "\uac8c\uc790\ub9ac", + "capricorn": "\uc5fc\uc18c\uc790\ub9ac", + "gemini": "\uc30d\ub465\uc774\uc790\ub9ac", + "leo": "\uc0ac\uc790\uc790\ub9ac", + "libra": "\ucc9c\uce6d\uc790\ub9ac", + "pisces": "\ubb3c\uace0\uae30\uc790\ub9ac", + "sagittarius": "\uad81\uc218\uc790\ub9ac", + "scorpio": "\uc804\uac08\uc790\ub9ac", + "taurus": "\ud669\uc18c\uc790\ub9ac", + "virgo": "\ucc98\ub140\uc790\ub9ac" } } } \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.nl.json b/homeassistant/components/zodiac/translations/sensor.nl.json index c07b20de21b..6dba645ed83 100644 --- a/homeassistant/components/zodiac/translations/sensor.nl.json +++ b/homeassistant/components/zodiac/translations/sensor.nl.json @@ -3,6 +3,7 @@ "zodiac__sign": { "aquarius": "Waterman", "aries": "Ram", + "cancer": "Kreeft", "capricorn": "Steenbok", "gemini": "Tweelingen", "leo": "Leo", diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index e1d48cbe1ff..4866c278074 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, Optional, cast +from typing import Any, Dict, cast import voluptuous as vol @@ -92,7 +92,7 @@ STORAGE_VERSION = 1 @bind_hass def async_active_zone( hass: HomeAssistant, latitude: float, longitude: float, radius: int = 0 -) -> Optional[State]: +) -> State | None: """Find the active zone for given latitude, longitude. This method must be run in the event loop. @@ -161,22 +161,22 @@ class ZoneStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: Dict) -> Dict: + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" return cast(Dict, self.CREATE_SCHEMA(data)) @callback - def _get_suggested_id(self, info: Dict) -> str: + def _get_suggested_id(self, info: dict) -> str: """Suggest an ID based on the config.""" return cast(str, info[CONF_NAME]) - async def _update_data(self, data: dict, update_data: Dict) -> Dict: + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return {**data, **update_data} -async def async_setup(hass: HomeAssistant, config: Dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up configured zones as well as Home Assistant zone if necessary.""" component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -240,7 +240,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: @callback -def _home_conf(hass: HomeAssistant) -> Dict: +def _home_conf(hass: HomeAssistant) -> dict: """Return the home zone config.""" return { CONF_NAME: hass.config.location_name, @@ -279,15 +279,15 @@ async def async_unload_entry( class Zone(entity.Entity): """Representation of a Zone.""" - def __init__(self, config: Dict): + def __init__(self, config: dict): """Initialize the zone.""" self._config = config self.editable = True - self._attrs: Optional[Dict] = None + self._attrs: dict | None = None self._generate_attrs() @classmethod - def from_yaml(cls, config: Dict) -> Zone: + def from_yaml(cls, config: dict) -> Zone: """Return entity instance initialized from yaml storage.""" zone = cls(config) zone.editable = False @@ -305,17 +305,17 @@ class Zone(entity.Entity): return cast(str, self._config[CONF_NAME]) @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return unique ID.""" return self._config.get(CONF_ID) @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon if any.""" return self._config.get(CONF_ICON) @property - def state_attributes(self) -> Optional[Dict]: + def extra_state_attributes(self) -> dict | None: """Return the state attributes of the zone.""" return self._attrs @@ -324,7 +324,7 @@ class Zone(entity.Entity): """Zone does not poll.""" return False - async def async_update_config(self, config: Dict) -> None: + async def async_update_config(self, config: dict) -> None: """Handle when the config is updated.""" if self._config == config: return diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index bb34a83ad26..de163146ab7 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -6,7 +6,7 @@ migrated to the storage collection. """ from homeassistant import config_entries -from .const import DOMAIN # noqa # pylint:disable=unused-import +from .const import DOMAIN class ZoneConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/zone/translations/id.json b/homeassistant/components/zone/translations/id.json index b84710dc408..aa05923b561 100644 --- a/homeassistant/components/zone/translations/id.json +++ b/homeassistant/components/zone/translations/id.json @@ -8,7 +8,7 @@ "data": { "icon": "Ikon", "latitude": "Lintang", - "longitude": "Garis bujur", + "longitude": "Bujur", "name": "Nama", "passive": "Pasif", "radius": "Radius" diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index a5f89a7515d..db5ca2cf01b 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -37,6 +37,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type: str = "zone" ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" + trigger_id = automation_info.get("trigger_id") if automation_info else None entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) @@ -80,6 +81,7 @@ async def async_attach_trigger( "zone": zone_state, "event": event, "description": description, + "id": trigger_id, } }, to_s.context, diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 75531e79e13..701f4b490d3 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -4,10 +4,9 @@ import logging import voluptuous as vol from zoneminder.monitor import TimePeriod -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from . import DOMAIN as ZONEMINDER_DOMAIN @@ -57,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(sensors) -class ZMSensorMonitors(Entity): +class ZMSensorMonitors(SensorEntity): """Get the status of each ZoneMinder monitor.""" def __init__(self, monitor): @@ -91,7 +90,7 @@ class ZMSensorMonitors(Entity): self._is_available = self._monitor.is_available -class ZMSensorEvents(Entity): +class ZMSensorEvents(SensorEntity): """Get the number of events for each monitor.""" def __init__(self, monitor, include_archived, sensor_type): @@ -122,7 +121,7 @@ class ZMSensorEvents(Entity): self._state = self._monitor.get_events(self.time_period, self._include_archived) -class ZMSensorRunState(Entity): +class ZMSensorRunState(SensorEntity): """Get the ZoneMinder run state.""" def __init__(self, client): diff --git a/homeassistant/components/zoneminder/translations/hu.json b/homeassistant/components/zoneminder/translations/hu.json index f1f99fa2f7c..a40d9299251 100644 --- a/homeassistant/components/zoneminder/translations/hu.json +++ b/homeassistant/components/zoneminder/translations/hu.json @@ -1,13 +1,28 @@ { "config": { "abort": { - "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez." + "auth_fail": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "create_entry": { "default": "ZoneMinder szerver hozz\u00e1adva." }, "error": { - "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez." + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/id.json b/homeassistant/components/zoneminder/translations/id.json new file mode 100644 index 00000000000..25f1d98fc26 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "auth_fail": "Nama pengguna atau kata sandi salah.", + "cannot_connect": "Gagal terhubung", + "connection_error": "Gagal terhubung ke server ZoneMinder.", + "invalid_auth": "Autentikasi tidak valid" + }, + "create_entry": { + "default": "Server ZoneMinder ditambahkan." + }, + "error": { + "auth_fail": "Nama pengguna atau kata sandi salah.", + "cannot_connect": "Gagal terhubung", + "connection_error": "Gagal terhubung ke server ZoneMinder.", + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "Host dan Port (mis. 10.10.0.4:8010)", + "password": "Kata Sandi", + "path": "Jalur ZM", + "path_zms": "Jalur ZMS", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "title": "Tambahkan Server ZoneMinder." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/ko.json b/homeassistant/components/zoneminder/translations/ko.json index e03da9ed8fa..bee0b7b6465 100644 --- a/homeassistant/components/zoneminder/translations/ko.json +++ b/homeassistant/components/zoneminder/translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "auth_fail": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -10,7 +10,7 @@ "default": "ZoneMinder \uc11c\ubc84\uac00 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "auth_fail": "\uc0ac\uc6a9\uc790\uba85\uacfc \uc554\ud638\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "auth_fail": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "connection_error": "ZoneMinder \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" @@ -19,15 +19,15 @@ "step": { "user": { "data": { - "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8(\uc608: 10.10.0.4:8010)", + "host": "\ud638\uc2a4\ud2b8 \ubc0f \ud3ec\ud2b8 (\uc608: 10.10.0.4:8010)", "password": "\ube44\ubc00\ubc88\ud638", - "path": "ZMS \uacbd\ub85c", + "path": "ZM \uacbd\ub85c", "path_zms": "ZMS \uacbd\ub85c", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, - "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud558\uc138\uc694." + "title": "ZoneMinder \uc11c\ubc84\ub97c \ucd94\uac00\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/zoneminder/translations/ru.json b/homeassistant/components/zoneminder/translations/ru.json index bee720ee09a..f520f0e29bd 100644 --- a/homeassistant/components/zoneminder/translations/ru.json +++ b/homeassistant/components/zoneminder/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\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 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." @@ -10,7 +10,7 @@ "default": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder." }, "error": { - "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "auth_fail": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\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 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 ZoneMinder.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." @@ -24,7 +24,7 @@ "path": "\u041f\u0443\u0442\u044c \u043a ZM", "path_zms": "\u041f\u0443\u0442\u044c \u043a ZMS", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "username": "\u041b\u043e\u0433\u0438\u043d", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, "title": "ZoneMinder" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 3acf361dd52..12ea668dce8 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) @@ -89,7 +90,7 @@ DEFAULT_CONF_INVERT_PERCENT = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 -SUPPORTED_PLATFORMS = [ +PLATFORMS = [ "binary_sensor", "climate", "cover", @@ -103,7 +104,7 @@ SUPPORTED_PLATFORMS = [ RENAME_NODE_SCHEMA = vol.Schema( { vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_NAME): cv.string, + vol.Required(ATTR_NAME): cv.string, vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, } ) @@ -112,7 +113,7 @@ RENAME_VALUE_SCHEMA = vol.Schema( { vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - vol.Required(const.ATTR_NAME): cv.string, + vol.Required(ATTR_NAME): cv.string, vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, } ) @@ -396,7 +397,6 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - # pylint: disable=import-error from openzwave.group import ZWaveGroup from openzwave.network import ZWaveNetwork from openzwave.option import ZWaveOption @@ -662,7 +662,7 @@ async def async_setup_entry(hass, config_entry): """Rename a node.""" node_id = service.data.get(const.ATTR_NODE_ID) node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - name = service.data.get(const.ATTR_NAME) + name = service.data.get(ATTR_NAME) node.name = name _LOGGER.info("Renamed Z-Wave node %d to %s", node_id, name) update_ids = service.data.get(const.ATTR_UPDATE_IDS) @@ -683,7 +683,7 @@ async def async_setup_entry(hass, config_entry): value_id = service.data.get(const.ATTR_VALUE_ID) node = network.nodes[node_id] # pylint: disable=unsubscriptable-object value = node.values[value_id] - name = service.data.get(const.ATTR_NAME) + name = service.data.get(ATTR_NAME) value.label = name _LOGGER.info( "Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name @@ -889,9 +889,7 @@ async def async_setup_entry(hass, config_entry): continue network.manager.pressButton(value.value_id) network.manager.releaseButton(value.value_id) - _LOGGER.info( - "Resetting meters on node %s instance %s....", node_id, instance - ) + _LOGGER.info("Resetting meters on node %s instance %s", node_id, instance) return _LOGGER.info( "Node %s on instance %s does not have resettable meters", node_id, instance @@ -915,7 +913,7 @@ async def async_setup_entry(hass, config_entry): def start_zwave(_service_or_event): """Startup Z-Wave network.""" - _LOGGER.info("Starting Z-Wave network...") + _LOGGER.info("Starting Z-Wave network") network.start() hass.bus.fire(const.EVENT_NETWORK_START) @@ -939,7 +937,7 @@ async def async_setup_entry(hass, config_entry): "Z-Wave not ready after %d seconds, continuing anyway", waited ) _LOGGER.info( - "final network state: %d %s", network.state, network.state_str + "Final network state: %d %s", network.state, network.state_str ) break @@ -1061,7 +1059,7 @@ async def async_setup_entry(hass, config_entry): hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave) - for entry_component in SUPPORTED_PLATFORMS: + for entry_component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, entry_component) ) @@ -1229,7 +1227,7 @@ class ZWaveDeviceEntityValues: return self._hass.data[DATA_DEVICES][device.unique_id] = device - if component in SUPPORTED_PLATFORMS: + if component in PLATFORMS: async_dispatcher_send(self._hass, f"zwave_new_{component}", device) else: await discovery.async_load_platform( @@ -1251,7 +1249,6 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def __init__(self, values, domain): """Initialize the z-Wave device.""" - # pylint: disable=import-error super().__init__() from openzwave.network import ZWaveNetwork from pydispatch import dispatcher @@ -1363,7 +1360,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attrs = { const.ATTR_NODE_ID: self.node_id, diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 9c9c1ed6128..75780eb314a 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -1,7 +1,8 @@ """Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI +from __future__ import annotations + import logging -from typing import Optional, Tuple from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -181,17 +182,19 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): int(self.node.manufacturer_id, 16), int(self.node.product_id, 16), ) - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120: - _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat workaround") - self._zxt_120 = 1 + if ( + specific_sensor_key in DEVICE_MAPPINGS + and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120 + ): + _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat workaround") + self._zxt_120 = 1 self.update_properties() def _mode(self) -> None: """Return thermostat mode Z-Wave value.""" raise NotImplementedError() - def _current_mode_setpoints(self) -> Tuple: + def _current_mode_setpoints(self) -> tuple: """Return a tuple of current setpoint Z-Wave value(s).""" raise NotImplementedError() @@ -483,12 +486,12 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): return self._target_temperature @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._target_temperature_range[0] @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" return self._target_temperature_range[1] @@ -566,14 +569,13 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): def set_swing_mode(self, swing_mode): """Set new target swing mode.""" _LOGGER.debug("Set swing_mode to %s", swing_mode) - if self._zxt_120 == 1: - if self.values.zxt_120_swing_mode: - self.values.zxt_120_swing_mode.data = swing_mode + if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: + self.values.zxt_120_swing_mode.data = swing_mode @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" - data = super().device_state_attributes + data = super().extra_state_attributes if self._fan_action: data[ATTR_FAN_ACTION] = self._fan_action return data @@ -590,7 +592,7 @@ class ZWaveClimateSingleSetpoint(ZWaveClimateBase): """Return thermostat mode Z-Wave value.""" return self.values.mode - def _current_mode_setpoints(self) -> Tuple: + def _current_mode_setpoints(self) -> tuple: """Return a tuple of current setpoint Z-Wave value(s).""" return (self.values.primary,) @@ -606,7 +608,7 @@ class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): """Return thermostat mode Z-Wave value.""" return self.values.primary - def _current_mode_setpoints(self) -> Tuple: + def _current_mode_setpoints(self) -> tuple: """Return a tuple of current setpoint Z-Wave value(s).""" current_mode = str(self.values.primary.data).lower() setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 83fb43fd3fb..d11d308c490 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -8,7 +8,6 @@ ATTR_INSTANCE = "instance" ATTR_GROUP = "group" ATTR_VALUE_ID = "value_id" ATTR_MESSAGES = "messages" -ATTR_NAME = "name" ATTR_RETURN_ROUTES = "return_routes" ATTR_SCENE_ID = "scene_id" ATTR_SCENE_DATA = "scene_data" diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 52014e37eea..140f601b1d9 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -138,10 +138,12 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): int(self.node.manufacturer_id, 16), int(self.node.product_id, 16), ) - if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098: - _LOGGER.debug("AEOTEC ZW098 workaround enabled") - self._zw098 = 1 + if ( + specific_sensor_key in DEVICE_MAPPINGS + and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098 + ): + _LOGGER.debug("AEOTEC ZW098 workaround enabled") + self._zw098 = 1 # Used for value change event handling self._refreshing = False diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index c9601679f57..bc49f9c0bd2 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -95,7 +95,7 @@ LOCK_ALARM_TYPE = { "27": "Auto re-lock", "33": "User deleted: ", "112": "Master code changed or User added: ", - "113": "Duplicate Pin-code: ", + "113": "Duplicate PIN code: ", "130": "RF module, power restored", "144": "Unlocked by NFC Tag or Card by user ", "161": "Tamper Alarm: ", @@ -291,17 +291,17 @@ class ZwaveLock(ZWaveDeviceEntity, LockEntity): if self._state_workaround: self._state = LOCK_STATUS.get(str(notification_data)) _LOGGER.debug("workaround: lock state set to %s", self._state) - if self._v2btze: - if ( - self.values.v2btze_advanced - and self.values.v2btze_advanced.data == CONFIG_ADVANCED - ): - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug( - "Lock state set from Access Control value and is %s, get=%s", - str(notification_data), - self.state, - ) + if ( + self._v2btze + and self.values.v2btze_advanced + and self.values.v2btze_advanced.data == CONFIG_ADVANCED + ): + self._state = LOCK_STATUS.get(str(notification_data)) + _LOGGER.debug( + "Lock state set from Access Control value and is %s, get=%s", + str(notification_data), + self.state, + ) if self._track_message_workaround: this_message = self.node.stats["lastReceivedMessage"][5] @@ -374,9 +374,9 @@ class ZwaveLock(ZWaveDeviceEntity, LockEntity): self.values.primary.data = False @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" - data = super().device_state_attributes + data = super().extra_state_attributes if self._notification: data[ATTR_NOTIFICATION] = self._notification if self._lock_status: diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index faaea30e0ee..3fa26439ad5 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -118,7 +118,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): def __init__(self, node, network): """Initialize node.""" - # pylint: disable=import-error super().__init__() from openzwave.network import ZWaveNetwork from pydispatch import dispatcher @@ -352,7 +351,7 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return self._name @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the device specific state attributes.""" attrs = { ATTR_NODE_ID: self.node_id, diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index aae38382f2e..a3183ba8927 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,5 +1,5 @@ """Support for Z-Wave sensors.""" -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN, SensorEntity from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -37,7 +37,7 @@ def get_device(node, values, **kwargs): return None -class ZWaveSensor(ZWaveDeviceEntity): +class ZWaveSensor(ZWaveDeviceEntity, SensorEntity): """Representation of a Z-Wave sensor.""" def __init__(self, values): diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json index 240e7fe776c..68a19863b53 100644 --- a/homeassistant/components/zwave/translations/hu.json +++ b/homeassistant/components/zwave/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { "option_error": "A Z-Wave \u00e9rv\u00e9nyes\u00edt\u00e9s sikertelen. Az USB-meghajt\u00f3 el\u00e9r\u00e9si \u00fatj\u00e1t helyesen adtad meg?" diff --git a/homeassistant/components/zwave/translations/id.json b/homeassistant/components/zwave/translations/id.json index 76c9c148b1e..99bd6270326 100644 --- a/homeassistant/components/zwave/translations/id.json +++ b/homeassistant/components/zwave/translations/id.json @@ -1,4 +1,23 @@ { + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "option_error": "Validasi Z-Wave gagal. Apakah jalur ke stik USB sudah benar?" + }, + "step": { + "user": { + "data": { + "network_key": "Kunci Jaringan (biarkan kosong untuk dibuat secara otomatis)", + "usb_path": "Jalur Perangkat USB" + }, + "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi", + "title": "Siapkan Z-Wave" + } + } + }, "state": { "_": { "dead": "Mati", @@ -7,8 +26,8 @@ "sleeping": "Tidur" }, "query_stage": { - "dead": "Mati ({query_stage})", - "initializing": "Inisialisasi ( {query_stage} )" + "dead": "Mati", + "initializing": "Inisialisasi" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json index 84c7b4ee4e3..674476ac759 100644 --- a/homeassistant/components/zwave/translations/ko.json +++ b/homeassistant/components/zwave/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "error": { "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json index 50e81003d27..a366d1d50df 100644 --- a/homeassistant/components/zwave/translations/nl.json +++ b/homeassistant/components/zwave/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Z-Wave is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { @@ -11,9 +11,9 @@ "user": { "data": { "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", - "usb_path": "USB-pad" + "usb_path": "USB-apparaatpad" }, - "description": "Zie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", + "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", "title": "Stel Z-Wave in" } } diff --git a/homeassistant/components/zwave_js/README.md b/homeassistant/components/zwave_js/README.md new file mode 100644 index 00000000000..920fc4a6a0b --- /dev/null +++ b/homeassistant/components/zwave_js/README.md @@ -0,0 +1,49 @@ +# Z-Wave JS Architecture + +This document describes the architecture of Z-Wave JS in Home Assistant and how the integration is connected all the way to the Z-Wave USB stick controller. + +## Architecture + +### Connection diagram + +![alt text][connection_diagram] + +#### Z-Wave USB stick + +Communicates with devices via the Z-Wave radio and stores device pairing. + +#### Z-Wave JS + +Represents the USB stick serial protocol as devices. + +#### Z-Wave JS Server + +Forward the state of Z-Wave JS over a WebSocket connection. + +#### Z-Wave JS Server Python + +Consumes the WebSocket connection and makes the Z-Wave JS state available in Python. + +#### Z-Wave JS integration + +Represents Z-Wave devices in Home Assistant and allows control. + +#### Home Assistant + +Best home automation platform in the world. + +### Running Z-Wave JS Server + +![alt text][running_zwave_js_server] + +Z-Wave JS Server can be run as a standalone Node app. + +It can also run as part of Z-Wave JS 2 MQTT, which is also a standalone Node app. + +Both apps are available as Home Assistant add-ons. There are also Docker containers etc. + +[connection_diagram]: docs/z_wave_js_connection.png "Connection Diagram" +[//]: # (https://docs.google.com/drawings/d/10yrczSRwV4kjQwzDnCLGoAJkePaB0BMVb1sWZeeDO7U/edit?usp=sharing) + +[running_zwave_js_server]: docs/running_z_wave_js_server.png "Running Z-Wave JS Server" +[//]: # (https://docs.google.com/drawings/d/1YhSVNuss3fa1VFTKQLaACxXg7y6qo742n2oYpdLRs7E/edit?usp=sharing) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 0da394721ac..10cc2543921 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,16 +1,26 @@ """The Z-Wave JS integration.""" +from __future__ import annotations + import asyncio -from typing import Callable, List +from typing import Callable from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.notification import Notification +from zwave_js_server.model.notification import ( + EntryControlNotification, + NotificationNotification, +) from zwave_js_server.model.value import ValueNotification from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DOMAIN, CONF_URL, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_DOMAIN, + CONF_URL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, entity_registry @@ -22,8 +32,12 @@ from .api import async_register_api from .const import ( ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, - ATTR_DEVICE_ID, + ATTR_DATA_TYPE, ATTR_ENDPOINT, + ATTR_EVENT, + ATTR_EVENT_DATA, + ATTR_EVENT_LABEL, + ATTR_EVENT_TYPE, ATTR_HOME_ID, ATTR_LABEL, ATTR_NODE_ID, @@ -45,7 +59,8 @@ from .const import ( EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, PLATFORMS, - ZWAVE_JS_EVENT, + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, ) from .discovery import async_discover_values from .helpers import get_device_id @@ -96,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) - dev_reg = await device_registry.async_get_registry(hass) + dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) @callback @@ -137,7 +152,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state - LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id) + LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) node.once( "ready", lambda event: async_on_node_ready(event["node"]), @@ -163,9 +178,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if notification.metadata.states: value = notification.metadata.states.get(str(value), value) hass.bus.async_fire( - ZWAVE_JS_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, { - ATTR_TYPE: "value_notification", ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, @@ -184,21 +198,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) @callback - def async_on_notification(notification: Notification) -> None: + def async_on_notification( + notification: EntryControlNotification | NotificationNotification, + ) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device({get_device_id(client, notification.node)}) - hass.bus.async_fire( - ZWAVE_JS_EVENT, - { - ATTR_TYPE: "notification", - ATTR_DOMAIN: DOMAIN, - ATTR_NODE_ID: notification.node.node_id, - ATTR_HOME_ID: client.driver.controller.home_id, - ATTR_DEVICE_ID: device.id, # type: ignore - ATTR_LABEL: notification.notification_label, - ATTR_PARAMETERS: notification.parameters, - }, - ) + event_data = { + ATTR_DOMAIN: DOMAIN, + ATTR_NODE_ID: notification.node.node_id, + ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_DEVICE_ID: device.id, # type: ignore + ATTR_COMMAND_CLASS: notification.command_class, + } + + if isinstance(notification, EntryControlNotification): + event_data.update( + { + ATTR_COMMAND_CLASS_NAME: "Entry Control", + ATTR_EVENT_TYPE: notification.event_type, + ATTR_DATA_TYPE: notification.data_type, + ATTR_EVENT_DATA: notification.event_data, + } + ) + else: + event_data.update( + { + ATTR_COMMAND_CLASS_NAME: "Notification", + ATTR_LABEL: notification.label, + ATTR_TYPE: notification.type_, + ATTR_EVENT: notification.event, + ATTR_EVENT_LABEL: notification.event_label, + ATTR_PARAMETERS: notification.parameters, + } + ) + + hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) # connect and throw error if connection failed @@ -222,7 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False - unsubscribe_callbacks: List[Callable] = [] + unsubscribe_callbacks: list[Callable] = [] entry_hass_data[DATA_CLIENT] = client entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks @@ -237,8 +271,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # wait until all required platforms are ready await asyncio.gather( *[ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS ] ) @@ -347,8 +381,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ] ) ) diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 818e46a34aa..0c2fdb17944 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from functools import partial -from typing import Any, Callable, Optional, TypeVar, cast +from typing import Any, Callable, TypeVar, cast from homeassistant.components.hassio import ( async_create_snapshot, @@ -66,9 +66,9 @@ class AddonManager: def __init__(self, hass: HomeAssistant) -> None: """Set up the add-on manager.""" self._hass = hass - self._install_task: Optional[asyncio.Task] = None - self._start_task: Optional[asyncio.Task] = None - self._update_task: Optional[asyncio.Task] = None + self._install_task: asyncio.Task | None = None + self._start_task: asyncio.Task | None = None + self._update_task: asyncio.Task | None = None def task_in_progress(self) -> bool: """Return True if any of the add-on tasks are in progress.""" diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index a48eadfad1d..eed04a34c7d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1,7 +1,8 @@ """Websocket API for Z-Wave JS.""" +from __future__ import annotations + import dataclasses import json -from typing import Dict from aiohttp import hdrs, web, web_exceptions import voluptuous as vol @@ -45,6 +46,10 @@ FILENAME = "filename" ENABLED = "enabled" FORCE_CONSOLE = "force_console" +# constants for setting config parameters +VALUE_ID = "value_id" +STATUS = "status" + @callback def async_register_api(hass: HomeAssistant) -> None: @@ -106,7 +111,12 @@ def websocket_node_status( entry_id = msg[ENTRY_ID] client = hass.data[DOMAIN][entry_id][DATA_CLIENT] node_id = msg[NODE_ID] - node = client.driver.controller.nodes[node_id] + node = client.driver.controller.nodes.get(node_id) + + if node is None: + connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") + return + data = { "node_id": node.node_id, "is_routing": node.is_routing, @@ -315,7 +325,7 @@ async def websocket_set_config_parameter( client = hass.data[DOMAIN][entry_id][DATA_CLIENT] node = client.driver.controller.nodes[node_id] try: - result = await async_set_config_parameter( + zwave_value, cmd_status = await async_set_config_parameter( node, value, property_, property_key=property_key ) except (InvalidNewValue, NotFoundError, NotImplementedError, SetValueFailed) as err: @@ -334,7 +344,10 @@ async def websocket_set_config_parameter( connection.send_result( msg[ID], - str(result), + { + VALUE_ID: zwave_value.value_id, + STATUS: cmd_status, + }, ) @@ -350,17 +363,23 @@ async def websocket_set_config_parameter( def websocket_get_config_parameters( hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: - """Get a list of configuration parameterss for a Z-Wave node.""" + """Get a list of configuration parameters for a Z-Wave node.""" entry_id = msg[ENTRY_ID] node_id = msg[NODE_ID] client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - node = client.driver.controller.nodes[node_id] + node = client.driver.controller.nodes.get(node_id) + + if node is None: + connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") + return + values = node.get_configuration_values() result = {} for value_id, zwave_value in values.items(): metadata = zwave_value.metadata result[value_id] = { "property": zwave_value.property_, + "property_key": zwave_value.property_key, "configuration_value_type": zwave_value.configuration_value_type.value, "metadata": { "description": metadata.description, @@ -383,12 +402,7 @@ def websocket_get_config_parameters( ) -def convert_log_level_to_enum(value: str) -> LogLevel: - """Convert log level string to LogLevel enum.""" - return LogLevel[value.upper()] - - -def filename_is_present_if_logging_to_file(obj: Dict) -> Dict: +def filename_is_present_if_logging_to_file(obj: dict) -> dict: """Validate that filename is provided if log_to_file is True.""" if obj.get(LOG_TO_FILE, False) and FILENAME not in obj: raise vol.Invalid("`filename` must be provided if logging to file") @@ -408,8 +422,8 @@ def filename_is_present_if_logging_to_file(obj: Dict) -> Dict: vol.Optional(LEVEL): vol.All( cv.string, vol.Lower, - vol.In([log_level.name.lower() for log_level in LogLevel]), - lambda val: LogLevel[val.upper()], + vol.In([log_level.value for log_level in LogLevel]), + lambda val: LogLevel(val), # pylint: disable=unnecessary-lambda ), vol.Optional(LOG_TO_FILE): cv.boolean, vol.Optional(FILENAME): cv.string, @@ -446,7 +460,7 @@ async def websocket_update_log_config( async def websocket_get_log_config( hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: - """Cancel removing a node from the Z-Wave network.""" + """Get log configuration for the Z-Wave JS driver.""" entry_id = msg[ENTRY_ID] client = hass.data[DOMAIN][entry_id][DATA_CLIENT] result = await client.driver.async_get_log_config() diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 8d266c83f22..b97975b0507 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -1,7 +1,8 @@ """Representation of Z-Wave binary sensors.""" +from __future__ import annotations import logging -from typing import Callable, List, Optional, TypedDict +from typing import Callable, TypedDict from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass @@ -56,14 +57,14 @@ class NotificationSensorMapping(TypedDict, total=False): """Represent a notification sensor mapping dict type.""" type: int # required - states: List[str] + states: list[str] device_class: str enabled: bool # Mappings for Notification sensors # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json -NOTIFICATION_SENSOR_MAPPINGS: List[NotificationSensorMapping] = [ +NOTIFICATION_SENSOR_MAPPINGS: list[NotificationSensorMapping] = [ { # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected "type": NOTIFICATION_SMOKE_ALARM, @@ -201,13 +202,13 @@ class PropertySensorMapping(TypedDict, total=False): """Represent a property sensor mapping dict type.""" property_name: str # required - on_states: List[str] # required + on_states: list[str] # required device_class: str enabled: bool # Mappings for property sensors -PROPERTY_SENSOR_MAPPINGS: List[PropertySensorMapping] = [ +PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [ { "property_name": PROPERTY_DOOR_STATUS, "on_states": ["open"], @@ -226,7 +227,7 @@ async def async_setup_entry( @callback def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Binary Sensor.""" - entities: List[BinarySensorEntity] = [] + entities: list[BinarySensorEntity] = [] if info.platform_hint == "notification": # Get all sensors from Notification CC states @@ -268,14 +269,14 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): self._name = self.generate_name(include_value_name=True) @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return if the sensor is on or off.""" if self.info.primary_value.value is None: return None return bool(self.info.primary_value.value) @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return device class.""" if self.info.primary_value.command_class == CommandClass.BATTERY: return DEVICE_CLASS_BATTERY @@ -284,12 +285,12 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - if self.info.primary_value.command_class == CommandClass.SENSOR_BINARY: - # Legacy binary sensors are phased out (replaced by notification sensors) - # Disable by default to not confuse users - if self.info.node.device_class.generic.key != 0x20: - return False - return True + # Legacy binary sensors are phased out (replaced by notification sensors) + # Disable by default to not confuse users + return bool( + self.info.primary_value.command_class != CommandClass.SENSOR_BINARY + or self.info.node.device_class.generic.key == 0x20 + ) class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): @@ -314,14 +315,14 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): self._mapping_info = self._get_sensor_mapping() @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return if the sensor is on or off.""" if self.info.primary_value.value is None: return None return int(self.info.primary_value.value) == int(self.state_key) @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return device class.""" return self._mapping_info.get("device_class") @@ -365,14 +366,14 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): self._name = self.generate_name(include_value_name=True) @property - def is_on(self) -> Optional[bool]: + def is_on(self) -> bool | None: """Return if the sensor is on or off.""" if self.info.primary_value.value is None: return None return self.info.primary_value.value in self._mapping_info["on_states"] @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return device class.""" return self._mapping_info.get("device_class") diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 4f62d5792fc..b814aef2a9d 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -1,5 +1,7 @@ """Representation of Z-Wave thermostats.""" -from typing import Any, Callable, Dict, List, Optional, cast +from __future__ import annotations + +from typing import Any, Callable, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -39,7 +41,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -50,7 +57,7 @@ from .entity import ZWaveBaseEntity # Map Z-Wave HVAC Mode to Home Assistant value # Note: We treat "auto" as "heat_cool" as most Z-Wave devices # report auto_changeover as auto without schedule support. -ZW_HVAC_MODE_MAP: Dict[int, str] = { +ZW_HVAC_MODE_MAP: dict[int, str] = { ThermostatMode.OFF: HVAC_MODE_OFF, ThermostatMode.HEAT: HVAC_MODE_HEAT, ThermostatMode.COOL: HVAC_MODE_COOL, @@ -67,7 +74,7 @@ ZW_HVAC_MODE_MAP: Dict[int, str] = { ThermostatMode.FULL_POWER: HVAC_MODE_HEAT, } -HVAC_CURRENT_MAP: Dict[int, str] = { +HVAC_CURRENT_MAP: dict[int, str] = { ThermostatOperatingState.IDLE: CURRENT_HVAC_IDLE, ThermostatOperatingState.PENDING_HEAT: CURRENT_HVAC_IDLE, ThermostatOperatingState.HEATING: CURRENT_HVAC_HEAT, @@ -94,7 +101,7 @@ async def async_setup_entry( @callback def async_add_climate(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Climate.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] entities.append(ZWaveClimate(config_entry, client, info)) async_add_entities(entities) @@ -116,23 +123,28 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): ) -> None: """Initialize lock.""" super().__init__(config_entry, client, info) - self._hvac_modes: Dict[str, Optional[int]] = {} - self._hvac_presets: Dict[str, Optional[int]] = {} - self._unit_value: Optional[ZwaveValue] = None + self._hvac_modes: dict[str, int | None] = {} + self._hvac_presets: dict[str, int | None] = {} + self._unit_value: ZwaveValue | None = None self._current_mode = self.get_zwave_value( THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE ) - self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {} + self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue] = {} for enum in ThermostatSetpointType: self._setpoint_values[enum] = self.get_zwave_value( THERMOSTAT_SETPOINT_PROPERTY, command_class=CommandClass.THERMOSTAT_SETPOINT, - value_property_key=enum.value.key, + value_property_key=enum.value, add_to_watched_value_ids=True, ) - # Use the first found setpoint value to always determine the temperature unit - if self._setpoint_values[enum] and not self._unit_value: + # Use the first found non N/A setpoint value to always determine the + # temperature unit + if ( + not self._unit_value + and enum != ThermostatSetpointType.NA + and self._setpoint_values[enum] + ): self._unit_value = self._setpoint_values[enum] self._operating_state = self.get_zwave_value( THERMOSTAT_OPERATING_STATE_PROPERTY, @@ -145,6 +157,8 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): add_to_watched_value_ids=True, check_all_endpoints=True, ) + if not self._unit_value: + self._unit_value = self._current_temp self._current_humidity = self.get_zwave_value( "Humidity", command_class=CommandClass.SENSOR_MULTILEVEL, @@ -162,7 +176,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): add_to_watched_value_ids=True, ) self._set_modes_and_presets() - self._supported_features = SUPPORT_PRESET_MODE + self._supported_features = 0 + if len(self._hvac_presets) > 1: + self._supported_features |= SUPPORT_PRESET_MODE # If any setpoint value exists, we can assume temperature # can be set if any(self._setpoint_values.values()): @@ -182,8 +198,8 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def _set_modes_and_presets(self) -> None: """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" - all_modes: Dict[str, Optional[int]] = {} - all_presets: Dict[str, Optional[int]] = {PRESET_NONE: None} + all_modes: dict[str, int | None] = {} + all_presets: dict[str, int | None] = {PRESET_NONE: None} # Z-Wave uses one list for both modes and presets. # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. @@ -206,7 +222,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._hvac_presets = all_presets @property - def _current_mode_setpoint_enums(self) -> List[Optional[ThermostatSetpointType]]: + def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType | None]: """Return the list of enums that are relevant to the current thermostat mode.""" if self._current_mode is None: # Thermostat(valve) with no support for setting a mode is considered heating-only @@ -224,6 +240,11 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return TEMP_FAHRENHEIT return TEMP_CELSIUS + @property + def precision(self) -> float: + """Return the precision of 0.1.""" + return PRECISION_TENTHS + @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -236,12 +257,12 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL) @property - def hvac_modes(self) -> List[str]: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return list(self._hvac_modes) @property - def hvac_action(self) -> Optional[str]: + def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" if not self._operating_state: return None @@ -251,17 +272,17 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return HVAC_CURRENT_MAP.get(int(self._operating_state.value)) @property - def current_humidity(self) -> Optional[int]: + def current_humidity(self) -> int | None: """Return the current humidity level.""" return self._current_humidity.value if self._current_humidity else None @property - def current_temperature(self) -> Optional[float]: + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._current_temp.value if self._current_temp else None @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self._current_mode and self._current_mode.value is None: # guard missing value @@ -273,7 +294,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return temp.value if temp else None @property - def target_temperature_high(self) -> Optional[float]: + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" if self._current_mode and self._current_mode.value is None: # guard missing value @@ -285,7 +306,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return temp.value if temp else None @property - def target_temperature_low(self) -> Optional[float]: + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" if self._current_mode and self._current_mode.value is None: # guard missing value @@ -295,25 +316,25 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return None @property - def preset_mode(self) -> Optional[str]: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" if self._current_mode and self._current_mode.value is None: # guard missing value return None if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES: return_val: str = self._current_mode.metadata.states.get( - self._current_mode.value + str(self._current_mode.value) ) return return_val return PRESET_NONE @property - def preset_modes(self) -> Optional[List[str]]: + def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return list(self._hvac_presets) @property - def fan_mode(self) -> Optional[str]: + def fan_mode(self) -> str | None: """Return the fan setting.""" if ( self._fan_mode @@ -324,14 +345,14 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): return None @property - def fan_modes(self) -> Optional[List[str]]: + def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" if self._fan_mode and self._fan_mode.metadata.states: return list(self._fan_mode.metadata.states.values()) return None @property - def device_state_attributes(self) -> Optional[Dict[str, str]]: + def extra_state_attributes(self) -> dict[str, str] | None: """Return the optional state attributes.""" if ( self._fan_state @@ -371,8 +392,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - assert self.hass - hvac_mode: Optional[str] = kwargs.get(ATTR_HVAC_MODE) + hvac_mode: str | None = kwargs.get(ATTR_HVAC_MODE) if hvac_mode is not None: await self.async_set_hvac_mode(hvac_mode) @@ -380,7 +400,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): setpoint: ZwaveValue = self._setpoint_value( self._current_mode_setpoint_enums[0] ) - target_temp: Optional[float] = kwargs.get(ATTR_TEMPERATURE) + target_temp: float | None = kwargs.get(ATTR_TEMPERATURE) if target_temp is not None: await self.info.node.async_set_value(setpoint, target_temp) elif len(self._current_mode_setpoint_enums) == 2: @@ -390,8 +410,8 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): setpoint_high: ZwaveValue = self._setpoint_value( self._current_mode_setpoint_enums[1] ) - target_temp_low: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_HIGH) + target_temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH) if target_temp_low is not None: await self.info.node.async_set_value(setpoint_low, target_temp_low) if target_temp_high is not None: diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 4929f7e7869..313f4e146a5 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -1,7 +1,9 @@ """Config flow for Z-Wave JS integration.""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Dict, Optional, cast +from typing import Any, cast import aiohttp from async_timeout import timeout @@ -16,7 +18,7 @@ from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from .addon import AddonError, AddonManager, get_addon_manager -from .const import ( # pylint:disable=unused-import +from .const import ( CONF_ADDON_DEVICE, CONF_ADDON_NETWORK_KEY, CONF_INTEGRATION_CREATED_ADDON, @@ -76,28 +78,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up flow instance.""" - self.network_key: Optional[str] = None - self.usb_path: Optional[str] = None + self.network_key: str | None = None + self.usb_path: str | None = None self.use_addon = False - self.ws_address: Optional[str] = None + self.ws_address: str | None = None # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False - self.install_task: Optional[asyncio.Task] = None - self.start_task: Optional[asyncio.Task] = None + self.install_task: asyncio.Task | None = None + self.start_task: asyncio.Task | None = None async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle the initial step.""" - assert self.hass # typing if is_hassio(self.hass): # type: ignore # no-untyped-call return await self.async_step_on_supervisor() return await self.async_step_manual() async def async_step_manual( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -134,8 +135,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_hassio( # type: ignore # override - self, discovery_info: Dict[str, Any] - ) -> Dict[str, Any]: + self, discovery_info: dict[str, Any] + ) -> dict[str, Any]: """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. @@ -152,8 +153,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_hassio_confirm() async def async_step_hassio_confirm( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Confirm the add-on discovery.""" if user_input is not None: return await self.async_step_on_supervisor( @@ -163,7 +164,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="hassio_confirm") @callback - def _async_create_entry_from_vars(self) -> Dict[str, Any]: + def _async_create_entry_from_vars(self) -> dict[str, Any]: """Return a config entry for the flow.""" return self.async_create_entry( title=TITLE, @@ -177,8 +178,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_on_supervisor( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -201,8 +202,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_install_addon() async def async_step_install_addon( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Install Z-Wave JS add-on.""" if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) @@ -221,18 +222,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_progress_done(next_step_id="configure_addon") async def async_step_install_failed( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") async def async_step_configure_addon( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Ask for config for Z-Wave JS add-on.""" addon_config = await self._async_get_addon_config() - errors: Dict[str, str] = {} + errors: dict[str, str] = {} if user_input is not None: self.network_key = user_input[CONF_NETWORK_KEY] @@ -263,10 +264,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_start_addon( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Start Z-Wave JS add-on.""" - assert self.hass if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) return self.async_show_progress( @@ -282,14 +282,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_progress_done(next_step_id="finish_addon_setup") async def async_step_start_failed( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Add-on start failed.""" return self.async_abort(reason="addon_start_failed") async def _async_start_addon(self) -> None: """Start the Z-Wave JS add-on.""" - assert self.hass addon_manager: AddonManager = get_addon_manager(self.hass) try: await addon_manager.async_schedule_start_addon() @@ -320,14 +319,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_finish_addon_setup( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. Set unique id and abort if already configured. """ - assert self.hass if not self.ws_address: discovery_info = await self._async_get_addon_discovery_info() self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ffd6031349a..1c9f78b1751 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -28,7 +28,8 @@ EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" LOGGER = logging.getLogger(__package__) # constants for events -ZWAVE_JS_EVENT = f"{DOMAIN}_event" +ZWAVE_JS_VALUE_NOTIFICATION_EVENT = f"{DOMAIN}_value_notification" +ZWAVE_JS_NOTIFICATION_EVENT = f"{DOMAIN}_notification" ATTR_NODE_ID = "node_id" ATTR_HOME_ID = "home_id" ATTR_ENDPOINT = "endpoint" @@ -38,15 +39,21 @@ ATTR_VALUE_RAW = "value_raw" ATTR_COMMAND_CLASS = "command_class" ATTR_COMMAND_CLASS_NAME = "command_class_name" ATTR_TYPE = "type" -ATTR_DEVICE_ID = "device_id" ATTR_PROPERTY_NAME = "property_name" ATTR_PROPERTY_KEY_NAME = "property_key_name" ATTR_PROPERTY = "property" ATTR_PROPERTY_KEY = "property_key" ATTR_PARAMETERS = "parameters" +ATTR_EVENT = "event" +ATTR_EVENT_LABEL = "event_label" +ATTR_EVENT_TYPE = "event_type" +ATTR_EVENT_DATA = "event_data" +ATTR_DATA_TYPE = "data_type" +ATTR_WAIT_FOR_RESULT = "wait_for_result" # service constants SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" +SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS = "bulk_set_partial_config_parameters" ATTR_CONFIG_PARAMETER = "parameter" ATTR_CONFIG_PARAMETER_BITMASK = "bitmask" @@ -56,4 +63,6 @@ SERVICE_REFRESH_VALUE = "refresh_value" ATTR_REFRESH_ALL_VALUES = "refresh_all_values" +SERVICE_SET_VALUE = "set_value" + ADDON_SLUG = "core_zwave_js" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index ff77bdb408d..25c69335ed1 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -1,6 +1,8 @@ """Support for Z-Wave cover devices.""" +from __future__ import annotations + import logging -from typing import Any, Callable, List, Optional +from typing import Any, Callable from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue @@ -42,7 +44,7 @@ async def async_setup_entry( @callback def async_add_cover(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave cover.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "motorized_barrier": entities.append(ZwaveMotorizedBarrier(config_entry, client, info)) else: @@ -72,7 +74,7 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return true if cover is closed.""" if self.info.primary_value.value is None: # guard missing value @@ -80,7 +82,7 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): return bool(self.info.primary_value.value == 0) @property - def current_cover_position(self) -> Optional[int]: + def current_cover_position(self) -> int | None: """Return the current position of cover where 0 means closed and 100 is fully open.""" if self.info.primary_value.value is None: # guard missing value @@ -130,31 +132,31 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): ) @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int | None: """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_GARAGE @property - def is_opening(self) -> Optional[bool]: + def is_opening(self) -> bool | None: """Return if the cover is opening or not.""" if self.info.primary_value.value is None: return None return bool(self.info.primary_value.value == BARRIER_STATE_OPENING) @property - def is_closing(self) -> Optional[bool]: + def is_closing(self) -> bool | None: """Return if the cover is closing or not.""" if self.info.primary_value.value is None: return None return bool(self.info.primary_value.value == BARRIER_STATE_CLOSING) @property - def is_closed(self) -> Optional[bool]: + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if self.info.primary_value.value is None: return None diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index f5f3d9e5c5b..17ae01aa9b2 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1,7 +1,8 @@ """Map Z-Wave nodes and values to Home Assistant entities.""" +from __future__ import annotations from dataclasses import dataclass -from typing import Generator, List, Optional, Set, Union +from typing import Any, Generator from zwave_js_server.const import CommandClass from zwave_js_server.model.device_class import DeviceClassItem @@ -22,7 +23,7 @@ class ZwaveDiscoveryInfo: # the home assistant platform for which an entity should be created platform: str # hint for the platform about this discovered entity - platform_hint: Optional[str] = "" + platform_hint: str | None = "" @dataclass @@ -35,13 +36,19 @@ class ZWaveValueDiscoverySchema: """ # [optional] the value's command class must match ANY of these values - command_class: Optional[Set[int]] = None + command_class: set[int] | None = None # [optional] the value's endpoint must match ANY of these values - endpoint: Optional[Set[int]] = None + endpoint: set[int] | None = None # [optional] the value's property must match ANY of these values - property: Optional[Set[Union[str, int]]] = None + property: set[str | int] | None = None + # [optional] the value's property name must match ANY of these values + property_name: set[str] | None = None + # [optional] the value's property key must match ANY of these values + property_key: set[str | int] | None = None + # [optional] the value's property key name must match ANY of these values + property_key_name: set[str] | None = None # [optional] the value's metadata_type must match ANY of these values - type: Optional[Set[str]] = None + type: set[str] | None = None @dataclass @@ -58,29 +65,57 @@ class ZWaveDiscoverySchema: # primary value belonging to this discovery scheme primary_value: ZWaveValueDiscoverySchema # [optional] hint for platform - hint: Optional[str] = None + hint: str | None = None # [optional] the node's manufacturer_id must match ANY of these values - manufacturer_id: Optional[Set[int]] = None + manufacturer_id: set[int] | None = None # [optional] the node's product_id must match ANY of these values - product_id: Optional[Set[int]] = None + product_id: set[int] | None = None # [optional] the node's product_type must match ANY of these values - product_type: Optional[Set[int]] = None + product_type: set[int] | None = None # [optional] the node's firmware_version must match ANY of these values - firmware_version: Optional[Set[str]] = None + firmware_version: set[str] | None = None # [optional] the node's basic device class must match ANY of these values - device_class_basic: Optional[Set[Union[str, int]]] = None + device_class_basic: set[str | int] | None = None # [optional] the node's generic device class must match ANY of these values - device_class_generic: Optional[Set[Union[str, int]]] = None + device_class_generic: set[str | int] | None = None # [optional] the node's specific device class must match ANY of these values - device_class_specific: Optional[Set[Union[str, int]]] = None + device_class_specific: set[str | int] | None = None # [optional] additional values that ALL need to be present on the node for this scheme to pass - required_values: Optional[List[ZWaveValueDiscoverySchema]] = None + required_values: list[ZWaveValueDiscoverySchema] | None = None # [optional] additional values that MAY NOT be present on the node for this scheme to pass - absent_values: Optional[List[ZWaveValueDiscoverySchema]] = None + absent_values: list[ZWaveValueDiscoverySchema] | None = None # [optional] bool to specify if this primary value may be discovered by multiple platforms allow_multi: bool = False +def get_config_parameter_discovery_schema( + property_: set[str | int] | None = None, + property_name: set[str] | None = None, + property_key: set[str | int] | None = None, + property_key_name: set[str] | None = None, + **kwargs: Any, +) -> ZWaveDiscoverySchema: + """ + Return a discovery schema for a config parameter. + + Supports all keyword arguments to ZWaveValueDiscoverySchema except platform, hint, + and primary_value. + """ + return ZWaveDiscoverySchema( + platform="sensor", + hint="config_parameter", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.CONFIGURATION}, + property=property_, + property_name=property_name, + property_key=property_key, + property_key_name=property_key_name, + type={"number"}, + ), + **kwargs, + ) + + SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SWITCH_MULTILEVEL}, property={"currentValue"}, @@ -115,6 +150,20 @@ DISCOVERY_SCHEMAS = [ product_type={0x0038}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # Inovelli LZW36 light / fan controller combo using switch multilevel CC + # The fan is endpoint 2, the light is endpoint 1. + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x031E}, + product_id={0x0001}, + product_type={0x000E}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + endpoint={2}, + property={"currentValue"}, + type={"number"}, + ), + ), # Fibaro Shutter Fibaro FGS222 ZWaveDiscoverySchema( platform="cover", @@ -147,6 +196,19 @@ DISCOVERY_SCHEMAS = [ product_type={0x0003}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= + # Door lock mode config parameter. Functionality equivalent to Notification CC + # list sensors. + get_config_parameter_discovery_schema( + property_name={"Door lock mode"}, + device_class_generic={"Entry Control"}, + device_class_specific={ + "Door Lock", + "Advanced Door Lock", + "Secure Keypad Door Lock", + "Secure Lockbox", + }, + ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks ZWaveDiscoverySchema( @@ -380,7 +442,6 @@ DISCOVERY_SCHEMAS = [ @callback def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: """Run discovery on ZWave node and return matching (primary) values.""" - # pylint: disable=too-many-nested-blocks for value in node.values.values(): for schema in DISCOVERY_SCHEMAS: # check manufacturer_id @@ -389,56 +450,64 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None and value.node.manufacturer_id not in schema.manufacturer_id ): continue + # check product_id if ( schema.product_id is not None and value.node.product_id not in schema.product_id ): continue + # check product_type if ( schema.product_type is not None and value.node.product_type not in schema.product_type ): continue + # check firmware_version if ( schema.firmware_version is not None and value.node.firmware_version not in schema.firmware_version ): continue + # check device_class_basic if not check_device_class( value.node.device_class.basic, schema.device_class_basic ): continue + # check device_class_generic if not check_device_class( value.node.device_class.generic, schema.device_class_generic ): continue + # check device_class_specific if not check_device_class( value.node.device_class.specific, schema.device_class_specific ): continue + # check primary value if not check_value(value, schema.primary_value): continue + # check additional required values - if schema.required_values is not None: - if not all( - any(check_value(val, val_scheme) for val in node.values.values()) - for val_scheme in schema.required_values - ): - continue + if schema.required_values is not None and not all( + any(check_value(val, val_scheme) for val in node.values.values()) + for val_scheme in schema.required_values + ): + continue + # check for values that may not be present - if schema.absent_values is not None: - if any( - any(check_value(val, val_scheme) for val in node.values.values()) - for val_scheme in schema.absent_values - ): - continue + if schema.absent_values is not None and any( + any(check_value(val, val_scheme) for val in node.values.values()) + for val_scheme in schema.absent_values + ): + continue + # all checks passed, this value belongs to an entity yield ZwaveDiscoveryInfo( node=value.node, @@ -446,6 +515,7 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None platform=schema.platform, platform_hint=schema.hint, ) + if not schema.allow_multi: # break out of loop, this value may not be discovered by other schemas/platforms break @@ -466,6 +536,24 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: # check property if schema.property is not None and value.property_ not in schema.property: return False + # check property_name + if ( + schema.property_name is not None + and value.property_name not in schema.property_name + ): + return False + # check property_key + if ( + schema.property_key is not None + and value.property_key not in schema.property_key + ): + return False + # check property_key_name + if ( + schema.property_key_name is not None + and value.property_key_name not in schema.property_key_name + ): + return False # check metadata_type if schema.type is not None and value.metadata.type not in schema.type: return False @@ -474,7 +562,7 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: @callback def check_device_class( - device_class: DeviceClassItem, required_value: Optional[Set[Union[str, int]]] + device_class: DeviceClassItem, required_value: set[str | int] | None ) -> bool: """Check if device class id or label matches.""" if required_value is None: diff --git a/homeassistant/components/zwave_js/docs/running_z_wave_js_server.png b/homeassistant/components/zwave_js/docs/running_z_wave_js_server.png new file mode 100644 index 00000000000..53b5bdd3f8f Binary files /dev/null and b/homeassistant/components/zwave_js/docs/running_z_wave_js_server.png differ diff --git a/homeassistant/components/zwave_js/docs/z_wave_js_connection.png b/homeassistant/components/zwave_js/docs/z_wave_js_connection.png new file mode 100644 index 00000000000..dd40a78728f Binary files /dev/null and b/homeassistant/components/zwave_js/docs/z_wave_js_connection.png differ diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 7620323d940..458cc721650 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,7 +1,7 @@ """Generic Z-Wave Entity Class.""" +from __future__ import annotations import logging -from typing import List, Optional, Union from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue, get_value_id @@ -46,7 +46,6 @@ class ZWaveBaseEntity(Entity): async def async_poll_value(self, refresh_all_values: bool) -> None: """Poll a value.""" - assert self.hass if not refresh_all_values: self.hass.async_create_task( self.info.node.async_poll_value(self.info.primary_value) @@ -75,7 +74,6 @@ class ZWaveBaseEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity is added.""" - assert self.hass # typing # Add value_changed callbacks. self.async_on_remove( self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) @@ -99,8 +97,9 @@ class ZWaveBaseEntity(Entity): def generate_name( self, include_value_name: bool = False, - alternate_value_name: Optional[str] = None, - additional_info: Optional[List[str]] = None, + alternate_value_name: str | None = None, + additional_info: list[str] | None = None, + name_suffix: str | None = None, ) -> str: """Generate entity name.""" if additional_info is None: @@ -110,6 +109,8 @@ class ZWaveBaseEntity(Entity): or self.info.node.device_config.description or f"Node {self.info.node.node_id}" ) + if name_suffix: + name = f"{name} {name_suffix}" if include_value_name: value_name = ( alternate_value_name @@ -169,13 +170,13 @@ class ZWaveBaseEntity(Entity): @callback def get_zwave_value( self, - value_property: Union[str, int], - command_class: Optional[int] = None, - endpoint: Optional[int] = None, - value_property_key: Optional[int] = None, + value_property: str | int, + command_class: int | None = None, + endpoint: int | None = None, + value_property_key: int | None = None, add_to_watched_value_ids: bool = True, check_all_endpoints: bool = False, - ) -> Optional[ZwaveValue]: + ) -> ZwaveValue | None: """Return specific ZwaveValue on this ZwaveNode.""" # use commandclass and endpoint from primary value if omitted return_value = None diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index ea17fbe4cff..100e400f9f7 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -1,6 +1,8 @@ """Support for Z-Wave fans.""" +from __future__ import annotations + import math -from typing import Any, Callable, List, Optional +from typing import Any, Callable from zwave_js_server.client import Client as ZwaveClient @@ -36,7 +38,7 @@ async def async_setup_entry( @callback def async_add_fan(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave fan.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] entities.append(ZwaveFan(config_entry, client, info)) async_add_entities(entities) @@ -52,7 +54,7 @@ async def async_setup_entry( class ZwaveFan(ZWaveBaseEntity, FanEntity): """Representation of a Z-Wave fan.""" - async def async_set_percentage(self, percentage: Optional[int]) -> None: + async def async_set_percentage(self, percentage: int | None) -> None: """Set the speed percentage of the fan.""" target_value = self.get_zwave_value("targetValue") @@ -68,9 +70,9 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): async def async_turn_on( self, - speed: Optional[str] = None, - percentage: Optional[int] = None, - preset_mode: Optional[str] = None, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs: Any, ) -> None: """Turn the device on.""" @@ -82,7 +84,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): await self.info.node.async_set_value(target_value, 0) @property - def is_on(self) -> Optional[bool]: # type: ignore + def is_on(self) -> bool | None: # type: ignore """Return true if device is on (speed above 0).""" if self.info.primary_value.value is None: # guard missing value @@ -90,7 +92,7 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): return bool(self.info.primary_value.value > 0) @property - def percentage(self) -> Optional[int]: + def percentage(self) -> int | None: """Return the current speed percentage.""" if self.info.primary_value.value is None: # guard missing value diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 16baeb816c2..d535a22394c 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -1,5 +1,7 @@ """Helper functions for Z-Wave JS integration.""" -from typing import List, Tuple, cast +from __future__ import annotations + +from typing import cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode @@ -19,13 +21,13 @@ def get_unique_id(home_id: str, value_id: str) -> str: @callback -def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: +def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]: """Get device registry identifier for Z-Wave node.""" return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") @callback -def get_home_and_node_id_from_device_id(device_id: Tuple[str, str]) -> List[str]: +def get_home_and_node_id_from_device_id(device_id: tuple[str, str]) -> list[str]: """ Get home ID and node ID for Z-Wave device registry entry. diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index b501ecb58e7..d809874c432 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -1,6 +1,8 @@ """Support for Z-Wave lights.""" +from __future__ import annotations + import logging -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ColorComponent, CommandClass @@ -85,9 +87,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._supports_color = False self._supports_white_value = False self._supports_color_temp = False - self._hs_color: Optional[Tuple[float, float]] = None - self._white_value: Optional[int] = None - self._color_temp: Optional[int] = None + self._hs_color: tuple[float, float] | None = None + self._white_value: int | None = None + self._color_temp: int | None = None self._min_mireds = 153 # 6500K as a safe default self._max_mireds = 370 # 2700K as a safe default self._supported_features = SUPPORT_BRIGHTNESS @@ -126,17 +128,17 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self.brightness > 0 @property - def hs_color(self) -> Optional[Tuple[float, float]]: + def hs_color(self) -> tuple[float, float] | None: """Return the hs color.""" return self._hs_color @property - def white_value(self) -> Optional[int]: + def white_value(self) -> int | None: """Return the white value of this light between 0..255.""" return self._white_value @property - def color_temp(self) -> Optional[int]: + def color_temp(self) -> int | None: """Return the color temperature.""" return self._color_temp @@ -151,7 +153,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self._max_mireds @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features @@ -220,7 +222,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Turn the light off.""" await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) - async def _async_set_colors(self, colors: Dict[ColorComponent, int]) -> None: + async def _async_set_colors(self, colors: dict[ColorComponent, int]) -> None: """Set (multiple) defined colors to given value(s).""" # prefer the (new) combined color property # https://github.com/zwave-js/node-zwave-js/pull/1782 @@ -245,12 +247,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): async def _async_set_color(self, color: ColorComponent, new_value: int) -> None: """Set defined color to given value.""" - property_key = color.value # actually set the new color value target_zwave_value = self.get_zwave_value( "targetColor", CommandClass.SWITCH_COLOR, - value_property_key=property_key.key, + value_property_key=color.value, ) if target_zwave_value is None: # guard for unsupported color @@ -258,7 +259,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): await self.info.node.async_set_value(target_zwave_value, new_value) async def _async_set_brightness( - self, brightness: Optional[int], transition: Optional[int] = None + self, brightness: int | None, transition: int | None = None ) -> None: """Set new brightness to light.""" if brightness is None: @@ -273,9 +274,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # setting a value requires setting targetValue await self.info.node.async_set_value(self._target_value, zwave_brightness) - async def _async_set_transition_duration( - self, duration: Optional[int] = None - ) -> None: + async def _async_set_transition_duration(self, duration: int | None = None) -> None: """Set the transition time for the brightness value.""" if self._dimming_duration is None: return @@ -315,27 +314,27 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): red_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key=ColorComponent.RED.value.key, + value_property_key=ColorComponent.RED.value, ) green_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key=ColorComponent.GREEN.value.key, + value_property_key=ColorComponent.GREEN.value, ) blue_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key=ColorComponent.BLUE.value.key, + value_property_key=ColorComponent.BLUE.value, ) ww_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key=ColorComponent.WARM_WHITE.value.key, + value_property_key=ColorComponent.WARM_WHITE.value, ) cw_val = self.get_zwave_value( "currentColor", CommandClass.SWITCH_COLOR, - value_property_key=ColorComponent.COLD_WHITE.value.key, + value_property_key=ColorComponent.COLD_WHITE.value, ) # prefer the (new) combined color property # https://github.com/zwave-js/node-zwave-js/pull/1782 diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 6f2a1a72c7d..0647885345b 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -1,6 +1,8 @@ """Representation of Z-Wave locks.""" +from __future__ import annotations + import logging -from typing import Any, Callable, Dict, List, Optional, Union +from typing import Any, Callable import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient @@ -28,7 +30,7 @@ from .entity import ZWaveBaseEntity LOGGER = logging.getLogger(__name__) -STATE_TO_ZWAVE_MAP: Dict[int, Dict[str, Union[int, bool]]] = { +STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = { CommandClass.DOOR_LOCK: { STATE_UNLOCKED: DoorLockMode.UNSECURED, STATE_LOCKED: DoorLockMode.SECURED, @@ -52,7 +54,7 @@ async def async_setup_entry( @callback def async_add_lock(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Lock.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] entities.append(ZWaveLock(config_entry, client, info)) async_add_entities(entities) @@ -88,7 +90,7 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): """Representation of a Z-Wave lock.""" @property - def is_locked(self) -> Optional[bool]: + def is_locked(self) -> bool | None: """Return true if the lock is locked.""" if self.info.primary_value.value is None: # guard missing value @@ -100,7 +102,7 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): ) == int(self.info.primary_value.value) async def _set_lock_state( - self, target_state: str, **kwargs: Dict[str, Any] + self, target_state: str, **kwargs: dict[str, Any] ) -> None: """Set the lock state.""" target_value: ZwaveValue = self.get_zwave_value( @@ -112,11 +114,11 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): STATE_TO_ZWAVE_MAP[self.info.primary_value.command_class][target_state], ) - async def async_lock(self, **kwargs: Dict[str, Any]) -> None: + async def async_lock(self, **kwargs: dict[str, Any]) -> None: """Lock the lock.""" await self._set_lock_state(STATE_LOCKED) - async def async_unlock(self, **kwargs: Dict[str, Any]) -> None: + async def async_unlock(self, **kwargs: dict[str, Any]) -> None: """Unlock the lock.""" await self._set_lock_state(STATE_UNLOCKED) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index de0f2cc0a6f..e6b4ed7c2a8 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.21.1"], + "requirements": ["zwave-js-server-python==0.23.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"] } diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 49c18073de5..997d34c8445 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -1,6 +1,7 @@ """Functions used to migrate unique IDs for Z-Wave JS entities.""" +from __future__ import annotations + import logging -from typing import List from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue @@ -35,8 +36,8 @@ def async_migrate_entity( except ValueError: _LOGGER.debug( ( - "Entity %s can't be migrated because the unique ID is taken. " - "Cleaning it up since it is likely no longer valid." + "Entity %s can't be migrated because the unique ID is taken; " + "Cleaning it up since it is likely no longer valid" ), entity_id, ) @@ -85,7 +86,7 @@ def async_migrate_discovered_value( @callback -def get_old_value_ids(value: ZwaveValue) -> List[str]: +def get_old_value_ids(value: ZwaveValue) -> list[str]: """Get old value IDs so we can migrate entity unique ID.""" value_ids = [] diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 8f8e894cda2..f418ee3d35b 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,5 +1,7 @@ """Support for Z-Wave controls using the number platform.""" -from typing import Callable, List, Optional +from __future__ import annotations + +from typing import Callable from zwave_js_server.client import Client as ZwaveClient @@ -22,7 +24,7 @@ async def async_setup_entry( @callback def async_add_number(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave number entity.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] entities.append(ZwaveNumberEntity(config_entry, client, info)) async_add_entities(entities) @@ -66,14 +68,14 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): return float(self.info.primary_value.metadata.max) @property - def value(self) -> Optional[float]: # type: ignore + def value(self) -> float | None: # type: ignore """Return the entity value.""" if self.info.primary_value.value is None: return None return float(self.info.primary_value.value) @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" if self.info.primary_value.metadata.unit is None: return None diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 8e22323c733..d1e18763b5b 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -1,10 +1,12 @@ """Representation of Z-Wave sensors.""" +from __future__ import annotations import logging -from typing import Callable, Dict, List, Optional +from typing import Callable, cast from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass +from zwave_js_server.const import CommandClass, ConfigurationValueType +from zwave_js_server.model.value import ConfigurationValue from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, @@ -12,9 +14,15 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, + SensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -34,7 +42,7 @@ async def async_setup_entry( @callback def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Sensor.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "string_sensor": entities.append(ZWaveStringSensor(config_entry, client, info)) @@ -42,6 +50,8 @@ async def async_setup_entry( entities.append(ZWaveNumericSensor(config_entry, client, info)) elif info.platform_hint == "list_sensor": entities.append(ZWaveListSensor(config_entry, client, info)) + elif info.platform_hint == "config_parameter": + entities.append(ZWaveConfigParameterSensor(config_entry, client, info)) else: LOGGER.warning( "Sensor not implemented for %s/%s", @@ -61,7 +71,7 @@ async def async_setup_entry( ) -class ZwaveSensorBase(ZWaveBaseEntity): +class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" def __init__( @@ -73,33 +83,45 @@ class ZwaveSensorBase(ZWaveBaseEntity): """Initialize a ZWaveSensorBase entity.""" super().__init__(config_entry, client, info) self._name = self.generate_name(include_value_name=True) + self._device_class = self._get_device_class() - @property - def device_class(self) -> Optional[str]: - """Return the device class of the sensor.""" + def _get_device_class(self) -> str | None: + """ + Get the device class of the sensor. + + This should be run once during initialization so we don't have to calculate + this value on every state update. + """ if self.info.primary_value.command_class == CommandClass.BATTERY: return DEVICE_CLASS_BATTERY if self.info.primary_value.command_class == CommandClass.METER: if self.info.primary_value.metadata.unit == "kWh": return DEVICE_CLASS_ENERGY return DEVICE_CLASS_POWER - if ( - isinstance(self.info.primary_value.property_, str) - and "temperature" in self.info.primary_value.property_.lower() - ): - return DEVICE_CLASS_TEMPERATURE + if isinstance(self.info.primary_value.property_, str): + property_lower = self.info.primary_value.property_.lower() + if "humidity" in property_lower: + return DEVICE_CLASS_HUMIDITY + if "temperature" in property_lower: + return DEVICE_CLASS_TEMPERATURE if self.info.primary_value.metadata.unit == "W": return DEVICE_CLASS_POWER if self.info.primary_value.metadata.unit == "Lux": return DEVICE_CLASS_ILLUMINANCE return None + @property + def device_class(self) -> str | None: + """Return the device class of the sensor.""" + return self._device_class + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" # We hide some of the more advanced sensors by default to not overwhelm users if self.info.primary_value.command_class in [ CommandClass.BASIC, + CommandClass.CONFIGURATION, CommandClass.INDICATOR, CommandClass.NOTIFICATION, ]: @@ -116,14 +138,14 @@ class ZWaveStringSensor(ZwaveSensorBase): """Representation of a Z-Wave String sensor.""" @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None return str(self.info.primary_value.value) @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if self.info.primary_value.metadata.unit is None: return None @@ -155,7 +177,7 @@ class ZWaveNumericSensor(ZwaveSensorBase): return round(float(self.info.primary_value.value), 2) @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if self.info.primary_value.metadata.unit is None: return None @@ -185,13 +207,13 @@ class ZWaveListSensor(ZwaveSensorBase): ) @property - def state(self) -> Optional[str]: + def state(self) -> str | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None if ( - not str(self.info.primary_value.value) - in self.info.primary_value.metadata.states + str(self.info.primary_value.value) + not in self.info.primary_value.metadata.states ): return str(self.info.primary_value.value) return str( @@ -199,7 +221,52 @@ class ZWaveListSensor(ZwaveSensorBase): ) @property - def device_state_attributes(self) -> Optional[Dict[str, str]]: + def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" # add the value's int value as property for multi-value (list) items return {"value": self.info.primary_value.value} + + +class ZWaveConfigParameterSensor(ZwaveSensorBase): + """Representation of a Z-Wave config parameter sensor.""" + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + ) -> None: + """Initialize a ZWaveConfigParameterSensor entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name( + include_value_name=True, + alternate_value_name=self.info.primary_value.property_name, + additional_info=[self.info.primary_value.property_key_name], + name_suffix="Config Parameter", + ) + self._primary_value = cast(ConfigurationValue, self.info.primary_value) + + @property + def state(self) -> str | None: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return None + if ( + self._primary_value.configuration_value_type == ConfigurationValueType.RANGE + or ( + not str(self.info.primary_value.value) + in self.info.primary_value.metadata.states + ) + ): + return str(self.info.primary_value.value) + return str( + self.info.primary_value.metadata.states[str(self.info.primary_value.value)] + ) + + @property + def extra_state_attributes(self) -> dict[str, str] | None: + """Return the device specific state attributes.""" + if self._primary_value.configuration_value_type == ConfigurationValueType.RANGE: + return None + # add the value's int value as property for multi-value (list) items + return {"value": self.info.primary_value.value} diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index c971891b35b..513abd97318 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -1,11 +1,17 @@ """Methods and classes related to executing Z-Wave commands and publishing these to hass.""" +from __future__ import annotations import logging -from typing import Dict, Set, Union import voluptuous as vol +from zwave_js_server.const import CommandStatus +from zwave_js_server.exceptions import SetValueFailed from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.util.node import async_set_config_parameter +from zwave_js_server.model.value import get_value_id +from zwave_js_server.util.node import ( + async_bulk_set_partial_config_parameters, + async_set_config_parameter, +) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -20,8 +26,8 @@ _LOGGER = logging.getLogger(__name__) def parameter_name_does_not_need_bitmask( - val: Dict[str, Union[int, str]] -) -> Dict[str, Union[int, str]]: + val: dict[str, int | str] +) -> dict[str, int | str]: """Validate that if a parameter name is provided, bitmask is not as well.""" if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and ( val.get(const.ATTR_CONFIG_PARAMETER_BITMASK) @@ -36,7 +42,13 @@ def parameter_name_does_not_need_bitmask( # Validates that a bitmask is provided in hex form and converts it to decimal # int equivalent since that's what the library uses BITMASK_SCHEMA = vol.All( - cv.string, vol.Lower, vol.Match(r"^(0x)?[0-9a-f]+$"), lambda value: int(value, 16) + cv.string, + vol.Lower, + vol.Match( + r"^(0x)?[0-9a-f]+$", + msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)", + ), + lambda value: int(value, 16), ) @@ -74,6 +86,30 @@ class ZWaveServices: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + self.async_bulk_set_partial_config_parameters, + schema=vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), + { + vol.Any(vol.Coerce(int), BITMASK_SCHEMA): vol.Any( + vol.Coerce(int), cv.string + ) + }, + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), + ) + self._hass.services.async_register( const.DOMAIN, const.SERVICE_REFRESH_VALUE, @@ -86,9 +122,32 @@ class ZWaveServices: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_SET_VALUE, + self.async_set_value, + schema=vol.Schema( + { + vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), + vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str), + vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(const.ATTR_VALUE): vol.Any( + bool, vol.Coerce(int), vol.Coerce(float), cv.string + ), + vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), + ) + async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" - nodes: Set[ZwaveNode] = set() + nodes: set[ZwaveNode] = set() if ATTR_ENTITY_ID in service.data: nodes |= { async_get_node_from_entity_id(self._hass, entity_id) @@ -104,26 +163,58 @@ class ZWaveServices: new_value = service.data[const.ATTR_CONFIG_VALUE] for node in nodes: - zwave_value = await async_set_config_parameter( + zwave_value, cmd_status = await async_set_config_parameter( node, new_value, property_or_property_name, property_key=property_key, ) - if zwave_value: - _LOGGER.info( - "Set configuration parameter %s on Node %s with value %s", - zwave_value, - node, - new_value, - ) + if cmd_status == CommandStatus.ACCEPTED: + msg = "Set configuration parameter %s on Node %s with value %s" else: - raise ValueError( - f"Unable to set configuration parameter on Node {node} with " - f"value {new_value}" + msg = ( + "Added command to queue to set configuration parameter %s on Node " + "%s with value %s. Parameter will be set when the device wakes up" ) + _LOGGER.info(msg, zwave_value, node, new_value) + + async def async_bulk_set_partial_config_parameters( + self, service: ServiceCall + ) -> None: + """Bulk set multiple partial config values on a node.""" + nodes: set[ZwaveNode] = set() + if ATTR_ENTITY_ID in service.data: + nodes |= { + async_get_node_from_entity_id(self._hass, entity_id) + for entity_id in service.data[ATTR_ENTITY_ID] + } + if ATTR_DEVICE_ID in service.data: + nodes |= { + async_get_node_from_device_id(self._hass, device_id) + for device_id in service.data[ATTR_DEVICE_ID] + } + property_ = service.data[const.ATTR_CONFIG_PARAMETER] + new_value = service.data[const.ATTR_CONFIG_VALUE] + + for node in nodes: + cmd_status = await async_bulk_set_partial_config_parameters( + node, + property_, + new_value, + ) + + if cmd_status == CommandStatus.ACCEPTED: + msg = "Bulk set partials for configuration parameter %s on Node %s" + else: + msg = ( + "Added command to queue to bulk set partials for configuration " + "parameter %s on Node %s" + ) + + _LOGGER.info(msg, property_, node) + async def async_poll_value(self, service: ServiceCall) -> None: """Poll value on a node.""" for entity_id in service.data[ATTR_ENTITY_ID]: @@ -137,3 +228,43 @@ class ZWaveServices: f"{const.DOMAIN}_{entry.unique_id}_poll_value", service.data[const.ATTR_REFRESH_ALL_VALUES], ) + + async def async_set_value(self, service: ServiceCall) -> None: + """Set a value on a node.""" + nodes: set[ZwaveNode] = set() + if ATTR_ENTITY_ID in service.data: + nodes |= { + async_get_node_from_entity_id(self._hass, entity_id) + for entity_id in service.data[ATTR_ENTITY_ID] + } + if ATTR_DEVICE_ID in service.data: + nodes |= { + async_get_node_from_device_id(self._hass, device_id) + for device_id in service.data[ATTR_DEVICE_ID] + } + command_class = service.data[const.ATTR_COMMAND_CLASS] + property_ = service.data[const.ATTR_PROPERTY] + property_key = service.data.get(const.ATTR_PROPERTY_KEY) + endpoint = service.data.get(const.ATTR_ENDPOINT) + new_value = service.data[const.ATTR_VALUE] + wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT) + + for node in nodes: + success = await node.async_set_value( + get_value_id( + node, + command_class, + property_, + endpoint=endpoint, + property_key=property_key, + ), + new_value, + wait_for_result=wait_for_result, + ) + + if success is False: + raise SetValueFailed( + "Unable to set value, refer to " + "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue " + "for possible reasons" + ) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index 7277f540d76..f9d90f94779 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -59,11 +59,37 @@ set_config_parameter: example: 5 required: true selector: - object: + text: bitmask: name: Bitmask description: Target a specific bitmask (see the documentation for more information). advanced: true + selector: + text: + +bulk_set_partial_config_parameters: + name: Bulk set partial configuration parameters for a Z-Wave device (Advanced). + description: Allow for bulk setting partial parameters. Useful when multiple partial parameters have to be set at the same time. + target: + entity: + integration: zwave_js + fields: + parameter: + name: Parameter + description: The id of the configuration parameter you want to configure. + example: 9 + required: true + selector: + text: + value: + name: Value + description: The new value(s) to set for this configuration parameter. Can either be a raw integer value to represent the bulk change or a mapping where the key is the bitmask (either in hex or integer form) and the value is the new value you want to set for that partial parameter. + example: + "0x1": 1 + "0x10": 1 + "0x20": 1 + "0x40": 1 + required: true selector: object: @@ -87,3 +113,53 @@ refresh_value: default: false selector: boolean: + +set_value: + name: Set a value on a Z-Wave device (Advanced) + description: Allow for changing any value that Z-Wave JS recognizes on a Z-Wave device. This service has minimal validation so only use this service if you know what you are doing. + target: + entity: + integration: zwave_js + fields: + command_class: + name: Command Class + description: The ID of the command class for the value. + example: 117 + required: true + selector: + text: + endpoint: + name: Endpoint + description: The endpoint for the value. + example: 1 + required: false + selector: + text: + property: + name: Property + description: The ID of the property for the value. + example: currentValue + required: true + selector: + text: + property_key: + name: Property Key + description: The ID of the property key for the value + example: 1 + required: false + selector: + text: + value: + name: Value + description: The new value to set. + example: "ffbb99" + required: true + selector: + object: + wait_for_result: + name: Wait for result? + description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device. + example: false + required: false + selector: + boolean: diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index a325e9821f7..e64ea57703d 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -1,7 +1,8 @@ """Representation of Z-Wave switches.""" +from __future__ import annotations import logging -from typing import Any, Callable, List, Optional +from typing import Any, Callable from zwave_js_server.client import Client as ZwaveClient @@ -30,7 +31,7 @@ async def async_setup_entry( @callback def async_add_switch(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Switch.""" - entities: List[ZWaveBaseEntity] = [] + entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "barrier_event_signaling_state": entities.append( ZWaveBarrierEventSignalingSwitch(config_entry, client, info) @@ -53,7 +54,7 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): """Representation of a Z-Wave switch.""" @property - def is_on(self) -> Optional[bool]: # type: ignore + def is_on(self) -> bool | None: # type: ignore """Return a boolean for the state of the switch.""" if self.info.primary_value.value is None: # guard missing value @@ -85,7 +86,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): """Initialize a ZWaveBarrierEventSignalingSwitch entity.""" super().__init__(config_entry, client, info) self._name = self.generate_name(include_value_name=True) - self._state: Optional[bool] = None + self._state: bool | None = None self._update_state() @@ -100,7 +101,7 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): return self._name @property - def is_on(self) -> Optional[bool]: # type: ignore + def is_on(self) -> bool | None: # type: ignore """Return a boolean for the state of the switch.""" return self._state diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json new file mode 100644 index 00000000000..abf89f00513 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "manual": { + "data": { + "url": "URL" + } + }, + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 9ff130605ef..ae9293cf926 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -1,30 +1,59 @@ { "config": { "abort": { + "addon_info_failed": "Fehler beim Abrufen von Z-Wave JS Add-on Informationen.", + "addon_install_failed": "Installation des Z-Wave JS Add-Ons fehlgeschlagen.", + "addon_set_config_failed": "Setzen der Z-Wave JS Konfiguration fehlgeschlagen", + "addon_start_failed": "Starten des Z-Wave JS Add-ons fehlgeschlagen.", "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { + "addon_start_failed": "Fehler beim Starten des Z-Wave JS Add-Ons. \u00dcberpr\u00fcfe die Konfiguration.", "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_ws_url": "Ung\u00fcltige Websocket-URL", "unknown": "Unerwarteter Fehler" }, + "progress": { + "install_addon": "Bitte warte, w\u00e4hrend die Installation des Z-Wave JS Add-ons abgeschlossen wird. Dies kann einige Minuten dauern.", + "start_addon": "Bitte warte, w\u00e4hrend der Start des Z-Wave JS Add-ons abgeschlossen wird. Dies kann einige Sekunden dauern." + }, "step": { "configure_addon": { "data": { + "network_key": "Netzwerk-Schl\u00fcssel", "usb_path": "USB-Ger\u00e4te-Pfad" - } + }, + "title": "Gib die Konfiguration des Z-Wave JS Add-ons ein" + }, + "hassio_confirm": { + "title": "Einrichten der Z-Wave JS Integration mit dem Z-Wave JS Add-on" + }, + "install_addon": { + "title": "Die Installation des Z-Wave-JS-Add-ons hat begonnen" }, "manual": { "data": { "url": "URL" } }, + "on_supervisor": { + "data": { + "use_addon": "Verwende das Z-Wave JS Supervisor Add-on" + }, + "description": "M\u00f6chtest du das Z-Wave JS Supervisor Add-on verwenden?", + "title": "Verbindungstyp ausw\u00e4hlen" + }, + "start_addon": { + "title": "Z-Wave JS Add-on wird gestartet." + }, "user": { "data": { "url": "URL" } } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 101942dc717..5be980d52cb 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -4,6 +4,7 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave JS add-on info.", "addon_install_failed": "Failed to install the Z-Wave JS add-on.", + "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "already_configured": "Device is already configured", @@ -48,6 +49,11 @@ }, "start_addon": { "title": "The Z-Wave JS add-on is starting." + }, + "user": { + "data": { + "url": "URL" + } } } }, diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 9cc8bf822b8..ce9f2f8b501 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -42,9 +42,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor" + "use_addon": "Utiliser le module compl\u00e9mentaire Z-Wave JS du Supervisor" }, - "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS Supervisor?", + "description": "Voulez-vous utiliser le module compl\u00e9mentaire Z-Wave JS du Supervisor?", "title": "S\u00e9lectionner la m\u00e9thode de connexion" }, "start_addon": { diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json new file mode 100644 index 00000000000..6732251f3a0 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_ws_url": "\u00c9rv\u00e9nytelen websocket URL", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "progress": { + "start_addon": "V\u00e1rj am\u00edg a Z-Wave JS b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + }, + "step": { + "configure_addon": { + "data": { + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + } + }, + "install_addon": { + "title": "Elkezd\u0151d\u00f6tt a Z-Wave JS b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Haszn\u00e1ld a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt" + }, + "description": "Szeretn\u00e9d haszn\u00e1lni az Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?", + "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" + }, + "start_addon": { + "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json new file mode 100644 index 00000000000..e8ea9381544 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/id.json @@ -0,0 +1,61 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.", + "addon_info_failed": "Gagal mendapatkan info add-on Z-Wave JS.", + "addon_install_failed": "Gagal menginstal add-on Z-Wave JS.", + "addon_missing_discovery_info": "Info penemuan add-on Z-Wave JS tidak ada.", + "addon_set_config_failed": "Gagal menyetel konfigurasi Z-Wave JS.", + "addon_start_failed": "Gagal memulai add-on Z-Wave JS.", + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "addon_start_failed": "Gagal memulai add-on Z-Wave JS. Periksa konfigurasi.", + "cannot_connect": "Gagal terhubung", + "invalid_ws_url": "URL websocket tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "progress": { + "install_addon": "Harap tunggu hingga penginstalan add-on Z-Wave JS selesai. Ini bisa memakan waktu beberapa saat.", + "start_addon": "Harap tunggu hingga add-on Z-Wave JS selesai. Ini mungkin perlu waktu beberapa saat." + }, + "step": { + "configure_addon": { + "data": { + "network_key": "Kunci Jaringan", + "usb_path": "Jalur Perangkat USB" + }, + "title": "Masukkan konfigurasi add-on Z-Wave JS" + }, + "hassio_confirm": { + "title": "Siapkan integrasi Z-Wave JS dengan add-on Z-Wave JS" + }, + "install_addon": { + "title": "Instalasi add-on Z-Wave JS telah dimulai" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Gunakan add-on Supervisor Z-Wave JS" + }, + "description": "Ingin menggunakan add-on Supervisor Z-Wave JS?", + "title": "Pilih metode koneksi" + }, + "start_addon": { + "title": "Add-on Z-Wave JS sedang dimulai." + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 9c86a064151..22149af7496 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -1,30 +1,61 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "addon_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "addon_install_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "addon_missing_discovery_info": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "addon_set_config_failed": "Z-Wave JS \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "error": { + "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_ws_url": "\uc6f9 \uc18c\ucf13 URL \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "progress": { + "install_addon": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uc124\uce58\uac00 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ubd84 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "start_addon": "Z-Wave JS \uc560\ub4dc\uc628 \uc2dc\uc791\uc774 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ucd08 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, "step": { "configure_addon": { "data": { + "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - } + }, + "title": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + }, + "hassio_confirm": { + "title": "Z-Wave JS \uc560\ub4dc\uc628\uc73c\ub85c Z-Wave JS \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30" + }, + "install_addon": { + "title": "Z-Wave JS \uc560\ub4dc\uc628 \uc124\uce58\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "manual": { "data": { "url": "URL \uc8fc\uc18c" } }, + "on_supervisor": { + "data": { + "use_addon": "Z-Wave JS Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uae30" + }, + "description": "Z-Wave JS Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc5f0\uacb0 \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "start_addon": { + "title": "Z-Wave JS \uc560\ub4dc\uc628\uc774 \uc2dc\uc791\ud558\ub294 \uc911\uc785\ub2c8\ub2e4." + }, "user": { "data": { "url": "URL \uc8fc\uc18c" } } } - } + }, + "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index c15cfd26f31..f50c9c8ceba 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -6,6 +6,7 @@ "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.", "addon_missing_discovery_info": "De Z-Wave JS addon mist ontdekkings informatie", "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", + "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.", "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken" @@ -17,7 +18,8 @@ "unknown": "Onverwachte fout" }, "progress": { - "install_addon": "Een ogenblik geduld terwijl de installatie van de Z-Wave JS add-on is voltooid. Dit kan enkele minuten duren." + "install_addon": "Een ogenblik geduld terwijl de installatie van de Z-Wave JS add-on is voltooid. Dit kan enkele minuten duren.", + "start_addon": "Wacht alstublieft terwijl de Z-Wave JS add-on start voltooid is. Dit kan enkele seconden duren." }, "step": { "configure_addon": { @@ -45,6 +47,9 @@ "description": "Wilt u de Z-Wave JS Supervisor add-on gebruiken?", "title": "Selecteer verbindingsmethode" }, + "start_addon": { + "title": "De add-on Z-Wave JS wordt gestart." + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index f1495b1aeda..10b003f71e8 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -1,25 +1,25 @@ { "config": { "abort": { - "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", - "addon_info_failed": "\u53d6\u5f97 Z-Wave JS add-on \u8cc7\u8a0a\u5931\u6557\u3002", - "addon_install_failed": "Z-Wave JS add-on \u5b89\u88dd\u5931\u6557\u3002", - "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u3002", - "addon_set_config_failed": "Z-Wave JS add-on \u8a2d\u5b9a\u5931\u6557\u3002", - "addon_start_failed": "Z-Wave JS add-on \u555f\u59cb\u5931\u6557\u3002", + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", + "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u3002", + "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", + "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002", "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { - "addon_start_failed": "Z-Wave JS add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002", + "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_ws_url": "Websocket URL \u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "progress": { - "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002", - "start_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002", + "start_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" }, "step": { "configure_addon": { @@ -27,13 +27,13 @@ "network_key": "\u7db2\u8def\u5bc6\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u8a2d\u5b9a" + "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" }, "hassio_confirm": { - "title": "\u4ee5 Z-Wave JS add-on \u8a2d\u5b9a Z-Wave JS \u6574\u5408" + "title": "\u4ee5 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a Z-Wave JS \u6574\u5408" }, "install_addon": { - "title": "Z-Wave JS add-on \u5b89\u88dd\u5df2\u555f\u52d5" + "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5" }, "manual": { "data": { @@ -42,13 +42,13 @@ }, "on_supervisor": { "data": { - "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor add-on" + "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6" }, - "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor add-on\uff1f", + "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" }, "start_addon": { - "title": "Z-Wave JS add-on \u555f\u59cb\u4e2d\u3002" + "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" }, "user": { "data": { diff --git a/homeassistant/config.py b/homeassistant/config.py index 90df365c349..362c93d04fa 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,11 +1,14 @@ """Module to help with parsing and generating configuration files.""" +from __future__ import annotations + from collections import OrderedDict import logging import os +from pathlib import Path import re import shutil from types import ModuleType -from typing import Any, Callable, Dict, Optional, Sequence, Set, Tuple, Union +from typing import Any, Callable, Sequence from awesomeversion import AwesomeVersion import voluptuous as vol @@ -59,7 +62,7 @@ from homeassistant.requirements import ( ) from homeassistant.util.package import is_docker_env from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.util.yaml import SECRET_YAML, load_yaml +from homeassistant.util.yaml import SECRET_YAML, Secrets, load_yaml _LOGGER = logging.getLogger(__name__) @@ -113,14 +116,14 @@ tts: def _no_duplicate_auth_provider( - configs: Sequence[Dict[str, Any]] -) -> Sequence[Dict[str, Any]]: + configs: Sequence[dict[str, Any]] +) -> Sequence[dict[str, Any]]: """No duplicate auth provider config allowed in a list. Each type of auth provider can only have one config without optional id. Unique id is required if same type of auth provider used multiple times. """ - config_keys: Set[Tuple[str, Optional[str]]] = set() + config_keys: set[tuple[str, str | None]] = set() for config in configs: key = (config[CONF_TYPE], config.get(CONF_ID)) if key in config_keys: @@ -134,8 +137,8 @@ def _no_duplicate_auth_provider( def _no_duplicate_auth_mfa_module( - configs: Sequence[Dict[str, Any]] -) -> Sequence[Dict[str, Any]]: + configs: Sequence[dict[str, Any]] +) -> Sequence[dict[str, Any]]: """No duplicate auth mfa module item allowed in a list. Each type of mfa module can only have one config without optional id. @@ -143,7 +146,7 @@ def _no_duplicate_auth_mfa_module( times. Note: this is different than auth provider """ - config_keys: Set[str] = set() + config_keys: set[str] = set() for config in configs: key = config.get(CONF_ID, config[CONF_TYPE]) if key in config_keys: @@ -312,29 +315,39 @@ def _write_default_config(config_dir: str) -> bool: return False -async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: +async def async_hass_config_yaml(hass: HomeAssistant) -> dict: """Load YAML from a Home Assistant configuration file. This function allow a component inside the asyncio loop to reload its configuration by itself. Include package merge. """ + if hass.config.config_dir is None: + secrets = None + else: + secrets = Secrets(Path(hass.config.config_dir)) + # Not using async_add_executor_job because this is an internal method. config = await hass.loop.run_in_executor( - None, load_yaml_config_file, hass.config.path(YAML_CONFIG_FILE) + None, + load_yaml_config_file, + hass.config.path(YAML_CONFIG_FILE), + secrets, ) core_config = config.get(CONF_CORE, {}) await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) return config -def load_yaml_config_file(config_path: str) -> Dict[Any, Any]: +def load_yaml_config_file( + config_path: str, secrets: Secrets | None = None +) -> dict[Any, Any]: """Parse a YAML configuration file. Raises FileNotFoundError or HomeAssistantError. This method needs to run in an executor. """ - conf_dict = load_yaml(config_path) + conf_dict = load_yaml(config_path, secrets) if not isinstance(conf_dict, dict): msg = ( @@ -410,9 +423,9 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: def async_log_exception( ex: Exception, domain: str, - config: Dict, + config: dict, hass: HomeAssistant, - link: Optional[str] = None, + link: str | None = None, ) -> None: """Log an error for configuration validation. @@ -426,8 +439,8 @@ def async_log_exception( @callback def _format_config_error( - ex: Exception, domain: str, config: Dict, link: Optional[str] = None -) -> Tuple[str, bool]: + ex: Exception, domain: str, config: dict, link: str | None = None +) -> tuple[str, bool]: """Generate log exception for configuration validation. This method must be run in the event loop. @@ -463,7 +476,7 @@ def _format_config_error( return message, is_friendly -async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None: +async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -592,7 +605,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non ) -def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> None: +def _log_pkg_error(package: str, component: str, config: dict, message: str) -> None: """Log an error while merging packages.""" message = f"Package {package} setup failed. Integration {component} {message}" @@ -605,7 +618,7 @@ def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> _LOGGER.error(message) -def _identify_config_schema(module: ModuleType) -> Optional[str]: +def _identify_config_schema(module: ModuleType) -> str | None: """Extract the schema and identify list or dict based.""" if not isinstance(module.CONFIG_SCHEMA, vol.Schema): # type: ignore return None @@ -653,9 +666,9 @@ def _identify_config_schema(module: ModuleType) -> Optional[str]: return None -def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[bool, str]: +def _recursive_merge(conf: dict[str, Any], package: dict[str, Any]) -> bool | str: """Merge package into conf, recursively.""" - error: Union[bool, str] = False + error: bool | str = False for key, pack_conf in package.items(): if isinstance(pack_conf, dict): if not pack_conf: @@ -677,10 +690,10 @@ def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[boo async def merge_packages_config( hass: HomeAssistant, - config: Dict, - packages: Dict[str, Any], + config: dict, + packages: dict[str, Any], _log_pkg_error: Callable = _log_pkg_error, -) -> Dict: +) -> dict: """Merge packages into the top-level configuration. Mutate config.""" PACKAGES_CONFIG_SCHEMA(packages) for pack_name, pack_conf in packages.items(): @@ -743,7 +756,7 @@ async def merge_packages_config( async def async_process_component_config( hass: HomeAssistant, config: ConfigType, integration: Integration -) -> Optional[ConfigType]: +) -> ConfigType | None: """Check component configuration and return processed configuration. Returns None on error. @@ -868,13 +881,13 @@ async def async_process_component_config( @callback -def config_without_domain(config: Dict, domain: str) -> Dict: +def config_without_domain(config: dict, domain: str) -> dict: """Return a config with all configuration for a domain removed.""" filter_keys = extract_domain_configs(config, domain) return {key: value for key, value in config.items() if key not in filter_keys} -async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: +async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: """Check if Home Assistant configuration file is valid. This method is a coroutine. @@ -891,7 +904,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: @callback def async_notify_setup_error( - hass: HomeAssistant, component: str, display_link: Optional[str] = None + hass: HomeAssistant, component: str, display_link: str | None = None ) -> None: """Print a persistent notification. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 12a795d0a51..23758cf88f2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -5,13 +5,14 @@ import asyncio import functools import logging from types import MappingProxyType, MethodType -from typing import Any, Callable, Dict, List, Optional, Set, Union, cast +from typing import Any, Callable, Optional, cast import weakref import attr from homeassistant import data_entry_flow, loader -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event @@ -143,11 +144,11 @@ class ConfigEntry: source: str, connection_class: str, system_options: dict, - options: Optional[dict] = None, - unique_id: Optional[str] = None, - entry_id: Optional[str] = None, + options: dict | None = None, + unique_id: str | None = None, + entry_id: str | None = None, state: str = ENTRY_STATE_NOT_LOADED, - disabled_by: Optional[str] = None, + disabled_by: str | None = None, ) -> None: """Initialize a config entry.""" # Unique id of the config entry @@ -190,18 +191,18 @@ class ConfigEntry: self.supports_unload = False # Listeners to call on update - self.update_listeners: List[ - Union[weakref.ReferenceType[UpdateListenerType], weakref.WeakMethod] + self.update_listeners: list[ + weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod ] = [] # Function to cancel a scheduled retry - self._async_cancel_retry_setup: Optional[Callable[[], Any]] = None + self._async_cancel_retry_setup: Callable[[], Any] | None = None async def async_setup( self, hass: HomeAssistant, *, - integration: Optional[loader.Integration] = None, + integration: loader.Integration | None = None, tries: int = 0, ) -> None: """Set up an entry.""" @@ -252,32 +253,43 @@ class ConfigEntry: "%s.async_setup_entry did not return boolean", integration.domain ) result = False - except ConfigEntryNotReady: + except ConfigEntryNotReady as ex: self.state = ENTRY_STATE_SETUP_RETRY wait_time = 2 ** min(tries, 4) * 5 tries += 1 + message = str(ex) + if not message and ex.__cause__: + message = str(ex.__cause__) + ready_message = f"ready yet: {message}" if message else "ready yet" if tries == 1: _LOGGER.warning( - "Config entry '%s' for %s integration not ready yet. Retrying in background", + "Config entry '%s' for %s integration not %s; Retrying in background", self.title, self.domain, + ready_message, ) else: _LOGGER.debug( - "Config entry '%s' for %s integration not ready yet. Retrying in %d seconds", + "Config entry '%s' for %s integration not %s; Retrying in %d seconds", self.title, self.domain, + ready_message, wait_time, ) - async def setup_again(now: Any) -> None: + async def setup_again(*_: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self.async_setup(hass, integration=integration, tries=tries) - self._async_cancel_retry_setup = hass.helpers.event.async_call_later( - wait_time, setup_again - ) + if hass.state == CoreState.running: + self._async_cancel_retry_setup = hass.helpers.event.async_call_later( + wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) return except Exception: # pylint: disable=broad-except _LOGGER.exception( @@ -295,7 +307,7 @@ class ConfigEntry: self.state = ENTRY_STATE_SETUP_ERROR async def async_unload( - self, hass: HomeAssistant, *, integration: Optional[loader.Integration] = None + self, hass: HomeAssistant, *, integration: loader.Integration | None = None ) -> bool: """Unload an entry. @@ -442,7 +454,7 @@ class ConfigEntry: return lambda: self.update_listeners.remove(weak_listener) - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: """Return dictionary version of this entry.""" return { "entry_id": self.entry_id, @@ -463,7 +475,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): """Manage all the config entry flows that are in progress.""" def __init__( - self, hass: HomeAssistant, config_entries: "ConfigEntries", hass_config: dict + self, hass: HomeAssistant, config_entries: ConfigEntries, hass_config: dict ): """Initialize the config entry flow manager.""" super().__init__(hass) @@ -471,8 +483,8 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): self._hass_config = hass_config 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: dict[str, Any] + ) -> dict[str, Any]: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) @@ -542,7 +554,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): return result async def async_create_flow( - self, handler_key: Any, *, context: Optional[Dict] = None, data: Any = None + self, handler_key: Any, *, context: dict | None = None, data: Any = None ) -> ConfigFlow: """Create a flow for specified handler. @@ -619,45 +631,47 @@ class ConfigEntries: self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.options = OptionsFlowManager(hass) self._hass_config = hass_config - self._entries: List[ConfigEntry] = [] + self._entries: dict[str, ConfigEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) EntityRegistryDisabledHandler(hass).async_setup() @callback - def async_domains(self) -> List[str]: + def async_domains( + self, include_ignore: bool = False, include_disabled: bool = False + ) -> list[str]: """Return domains for which we have entries.""" - seen: Set[str] = set() - result = [] - - for entry in self._entries: - if entry.domain not in seen: - seen.add(entry.domain) - result.append(entry.domain) - - return result + return list( + { + entry.domain: None + for entry in self._entries.values() + if (include_ignore or entry.source != SOURCE_IGNORE) + and (include_disabled or not entry.disabled_by) + } + ) @callback - def async_get_entry(self, entry_id: str) -> Optional[ConfigEntry]: + def async_get_entry(self, entry_id: str) -> ConfigEntry | None: """Return entry with matching entry_id.""" - for entry in self._entries: - if entry_id == entry.entry_id: - return entry - return None + return self._entries.get(entry_id) @callback - def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]: + def async_entries(self, domain: str | None = None) -> list[ConfigEntry]: """Return all entries or entries for a specific domain.""" if domain is None: - return list(self._entries) - return [entry for entry in self._entries if entry.domain == domain] + return list(self._entries.values()) + return [entry for entry in self._entries.values() if entry.domain == domain] async def async_add(self, entry: ConfigEntry) -> None: """Add and setup an entry.""" - self._entries.append(entry) + if entry.entry_id in self._entries: + raise HomeAssistantError( + f"An entry with the id {entry.entry_id} already exists." + ) + self._entries[entry.entry_id] = entry await self.async_setup(entry.entry_id) self._async_schedule_save() - async def async_remove(self, entry_id: str) -> Dict[str, Any]: + async def async_remove(self, entry_id: str) -> dict[str, Any]: """Remove an entry.""" entry = self.async_get_entry(entry_id) @@ -671,7 +685,7 @@ class ConfigEntries: await entry.async_remove(self.hass) - self._entries.remove(entry) + del self._entries[entry.entry_id] self._async_schedule_save() dev_reg, ent_reg = await asyncio.gather( @@ -707,11 +721,11 @@ class ConfigEntries: ) if config is None: - self._entries = [] + self._entries = {} return - self._entries = [ - ConfigEntry( + self._entries = { + entry["entry_id"]: ConfigEntry( version=entry["version"], domain=entry["domain"], entry_id=entry["entry_id"], @@ -730,7 +744,7 @@ class ConfigEntries: disabled_by=entry.get("disabled_by"), ) for entry in config["entries"] - ] + } async def async_setup(self, entry_id: str) -> bool: """Set up a config entry. @@ -789,7 +803,7 @@ class ConfigEntries: return await self.async_setup(entry_id) async def async_set_disabled_by( - self, entry_id: str, disabled_by: Optional[str] + self, entry_id: str, disabled_by: str | None ) -> bool: """Disable an entry. @@ -829,11 +843,11 @@ class ConfigEntries: self, entry: ConfigEntry, *, - unique_id: Union[str, dict, None, UndefinedType] = UNDEFINED, - title: Union[str, dict, UndefinedType] = UNDEFINED, - data: Union[dict, UndefinedType] = UNDEFINED, - options: Union[dict, UndefinedType] = UNDEFINED, - system_options: Union[dict, UndefinedType] = UNDEFINED, + unique_id: str | dict | None | UndefinedType = UNDEFINED, + title: str | dict | UndefinedType = UNDEFINED, + data: dict | UndefinedType = UNDEFINED, + options: dict | UndefinedType = UNDEFINED, + system_options: dict | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -918,12 +932,12 @@ class ConfigEntries: self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: + def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data to save.""" - return {"entries": [entry.as_dict() for entry in self._entries]} + return {"entries": [entry.as_dict() for entry in self._entries.values()]} -async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]: +async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]: """Migrate the pre-0.73 config format to the latest version.""" return {"entries": old_config} @@ -931,7 +945,7 @@ async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]: class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" - def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: + def __init_subclass__(cls, domain: str | None = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) # type: ignore if domain is not None: @@ -940,9 +954,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): CONNECTION_CLASS = CONN_CLASS_UNKNOWN @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return unique ID if available.""" - # pylint: disable=no-member if not self.context: return None @@ -957,7 +970,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _abort_if_unique_id_configured( self, - updates: Optional[Dict[Any, Any]] = None, + updates: dict[Any, Any] | None = None, reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" @@ -984,14 +997,14 @@ class ConfigFlow(data_entry_flow.FlowHandler): raise data_entry_flow.AbortFlow("already_configured") async def async_set_unique_id( - self, unique_id: Optional[str] = None, *, raise_on_progress: bool = True - ) -> Optional[ConfigEntry]: + self, unique_id: str | None = None, *, raise_on_progress: bool = True + ) -> ConfigEntry | None: """Set a unique ID for the config flow. Returns optionally existing config entry with same ID. """ if unique_id is None: - self.context["unique_id"] = None # pylint: disable=no-member + self.context["unique_id"] = None return None if raise_on_progress: @@ -999,7 +1012,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): if progress["context"].get("unique_id") == unique_id: raise data_entry_flow.AbortFlow("already_in_progress") - self.context["unique_id"] = unique_id # pylint: disable=no-member + self.context["unique_id"] = unique_id # Abort discoveries done using the default discovery unique id if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID: @@ -1014,20 +1027,33 @@ class ConfigFlow(data_entry_flow.FlowHandler): return None @callback - def _async_current_entries(self, include_ignore: bool = False) -> List[ConfigEntry]: + def _set_confirm_only( + self, + ) -> None: + """Mark the config flow as only needing user confirmation to finish flow.""" + self.context["confirm_only"] = True + + @callback + def _async_current_entries( + self, include_ignore: bool | None = None + ) -> list[ConfigEntry]: """Return current entries. If the flow is user initiated, filter out ignored entries unless include_ignore is True. """ config_entries = self.hass.config_entries.async_entries(self.handler) - if include_ignore or self.source != SOURCE_USER: + if ( + include_ignore is True + or include_ignore is None + and self.source != SOURCE_USER + ): return config_entries return [entry for entry in config_entries if entry.source != SOURCE_IGNORE] @callback - def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: + def _async_current_ids(self, include_ignore: bool = True) -> set[str | None]: """Return current unique IDs.""" return { entry.unique_id @@ -1036,26 +1062,28 @@ class ConfigFlow(data_entry_flow.FlowHandler): } @callback - def _async_in_progress(self) -> List[Dict]: + def _async_in_progress(self, include_uninitialized: bool = False) -> list[dict]: """Return other in progress flows for current domain.""" return [ flw - for flw in self.hass.config_entries.flow.async_progress() + for flw in self.hass.config_entries.flow.async_progress( + include_uninitialized=include_uninitialized + ) if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id ] - async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + async def async_step_ignore(self, user_input: dict[str, Any]) -> dict[str, Any]: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) return self.async_create_entry(title=user_input["title"], data={}) - async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + async def async_step_unignore(self, user_input: dict[str, Any]) -> dict[str, Any]: """Rediscover a config entry by it's unique_id.""" return self.async_abort(reason="not_implemented") async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initiated by the user.""" return self.async_abort(reason="not_implemented") @@ -1080,20 +1108,20 @@ class ConfigFlow(data_entry_flow.FlowHandler): self._abort_if_unique_id_configured() # Abort if any other flow for this handler is already in progress - if self._async_in_progress(): + if self._async_in_progress(include_uninitialized=True): raise data_entry_flow.AbortFlow("already_in_progress") async def async_step_discovery( - self, discovery_info: Dict[str, Any] - ) -> Dict[str, Any]: + self, discovery_info: dict[str, Any] + ) -> dict[str, Any]: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() @callback def async_abort( - self, *, reason: str, description_placeholders: Optional[Dict] = None - ) -> Dict[str, Any]: + self, *, reason: str, description_placeholders: dict | None = None + ) -> dict[str, Any]: """Abort the config flow.""" # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( @@ -1124,8 +1152,8 @@ class OptionsFlowManager(data_entry_flow.FlowManager): self, handler_key: Any, *, - context: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> OptionsFlow: """Create an options flow for a config entry. @@ -1141,8 +1169,8 @@ class OptionsFlowManager(data_entry_flow.FlowManager): return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) 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: dict[str, Any] + ) -> dict[str, Any]: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. @@ -1178,7 +1206,7 @@ class SystemOptions: """Update properties.""" self.disable_new_entities = disable_new_entities - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: """Return dictionary version of this config entries system options.""" return {"disable_new_entities": self.disable_new_entities} @@ -1189,9 +1217,9 @@ class EntityRegistryDisabledHandler: def __init__(self, hass: HomeAssistant) -> None: """Initialize the handler.""" self.hass = hass - self.registry: Optional[entity_registry.EntityRegistry] = None - self.changed: Set[str] = set() - self._remove_call_later: Optional[Callable[[], None]] = None + self.registry: entity_registry.EntityRegistry | None = None + self.changed: set[str] = set() + self._remove_call_later: Callable[[], None] | None = None @callback def async_setup(self) -> None: diff --git a/homeassistant/const.py b/homeassistant/const.py index d7bb7ea25b5..01efb971fdc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 -MINOR_VERSION = 3 -PATCH_VERSION = "4" +MINOR_VERSION = 4 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) @@ -73,6 +73,7 @@ CONF_CUSTOMIZE_GLOB = "customize_glob" CONF_DEFAULT = "default" CONF_DELAY = "delay" CONF_DELAY_TIME = "delay_time" +CONF_DESCRIPTION = "description" CONF_DEVICE = "device" CONF_DEVICES = "devices" CONF_DEVICE_CLASS = "device_class" @@ -209,7 +210,6 @@ EVENT_HOMEASSISTANT_STARTED = "homeassistant_started" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write" EVENT_LOGBOOK_ENTRY = "logbook_entry" -EVENT_PLATFORM_DISCOVERED = "platform_discovered" EVENT_SERVICE_REGISTERED = "service_registered" EVENT_SERVICE_REMOVED = "service_removed" EVENT_STATE_CHANGED = "state_changed" @@ -220,6 +220,8 @@ EVENT_TIME_CHANGED = "time_changed" # #### DEVICE CLASSES #### DEVICE_CLASS_BATTERY = "battery" +DEVICE_CLASS_CO = "carbon_monoxide" +DEVICE_CLASS_CO2 = "carbon_dioxide" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" @@ -312,9 +314,6 @@ CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" # Electrical attributes ATTR_VOLTAGE = "voltage" -# Contains the information that is discovered -ATTR_DISCOVERED = "discovered" - # Location of the device/sensor ATTR_LOCATION = "location" diff --git a/homeassistant/core.py b/homeassistant/core.py index b62dd1ee7d5..fdf2a093928 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -4,6 +4,8 @@ Core components of Home Assistant. Home Assistant is a Home Automation framework for observing the state of entities and react to changes. """ +from __future__ import annotations + import asyncio import datetime import enum @@ -22,15 +24,10 @@ from typing import ( Callable, Collection, Coroutine, - Dict, Iterable, - List, Mapping, Optional, - Set, - Tuple, TypeVar, - Union, cast, ) @@ -119,7 +116,7 @@ TIMEOUT_EVENT_START = 15 _LOGGER = logging.getLogger(__name__) -def split_entity_id(entity_id: str) -> List[str]: +def split_entity_id(entity_id: str) -> list[str]: """Split a state entity ID into domain and object ID.""" return entity_id.split(".", 1) @@ -215,9 +212,9 @@ class CoreState(enum.Enum): class HomeAssistant: """Root object of the Home Assistant home automation.""" - auth: "AuthManager" - http: "HomeAssistantHTTP" = None # type: ignore - config_entries: "ConfigEntries" = None # type: ignore + auth: AuthManager + http: HomeAssistantHTTP = None # type: ignore + config_entries: ConfigEntries = None # type: ignore def __init__(self) -> None: """Initialize new Home Assistant object.""" @@ -235,7 +232,7 @@ class HomeAssistant: self.state: CoreState = CoreState.not_running self.exit_code: int = 0 # If not None, use to signal end-of-loop - self._stopped: Optional[asyncio.Event] = None + self._stopped: asyncio.Event | None = None # Timeout handler for Core/Helper namespace self.timeout: TimeoutManager = TimeoutManager() @@ -340,7 +337,7 @@ class HomeAssistant: @callback def async_add_job( self, target: Callable[..., Any], *args: Any - ) -> Optional[asyncio.Future]: + ) -> asyncio.Future | None: """Add a job from within the event loop. This method must be run in the event loop. @@ -357,9 +354,7 @@ class HomeAssistant: return self.async_add_hass_job(HassJob(target), *args) @callback - def async_add_hass_job( - self, hassjob: HassJob, *args: Any - ) -> Optional[asyncio.Future]: + def async_add_hass_job(self, hassjob: HassJob, *args: Any) -> asyncio.Future | None: """Add a HassJob from within the event loop. This method must be run in the event loop. @@ -421,9 +416,7 @@ class HomeAssistant: self._track_task = False @callback - def async_run_hass_job( - self, hassjob: HassJob, *args: Any - ) -> Optional[asyncio.Future]: + def async_run_hass_job(self, hassjob: HassJob, *args: Any) -> asyncio.Future | None: """Run a HassJob from within the event loop. This method must be run in the event loop. @@ -439,8 +432,8 @@ class HomeAssistant: @callback def async_run_job( - self, target: Callable[..., Union[None, Awaitable]], *args: Any - ) -> Optional[asyncio.Future]: + self, target: Callable[..., None | Awaitable], *args: Any + ) -> asyncio.Future | None: """Run a job from within the event loop. This method must be run in the event loop. @@ -463,7 +456,7 @@ class HomeAssistant: """Block until all pending work is done.""" # To flush out any call_soon_threadsafe await asyncio.sleep(0) - start_time: Optional[float] = None + start_time: float | None = None while self._pending_tasks: pending = [task for task in self._pending_tasks if not task.done()] @@ -520,11 +513,13 @@ class HomeAssistant: if self.state == CoreState.not_running: # just ignore return if self.state in [CoreState.stopping, CoreState.final_write]: - _LOGGER.info("async_stop called twice: ignored") + _LOGGER.info("Additional call to async_stop was ignored") return if self.state == CoreState.starting: # This may not work - _LOGGER.warning("async_stop called before startup is complete") + _LOGGER.warning( + "Stopping Home Assistant before startup has completed may fail" + ) # stage 1 self.state = CoreState.stopping @@ -580,10 +575,10 @@ class Context: """The context that triggered something.""" user_id: str = attr.ib(default=None) - parent_id: Optional[str] = attr.ib(default=None) + parent_id: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) - def as_dict(self) -> Dict[str, Optional[str]]: + def as_dict(self) -> dict[str, str | None]: """Return a dictionary representation of the context.""" return {"id": self.id, "parent_id": self.parent_id, "user_id": self.user_id} @@ -607,10 +602,10 @@ class Event: def __init__( self, event_type: str, - data: Optional[Dict[str, Any]] = None, + data: dict[str, Any] | None = None, origin: EventOrigin = EventOrigin.local, - time_fired: Optional[datetime.datetime] = None, - context: Optional[Context] = None, + time_fired: datetime.datetime | None = None, + context: Context | None = None, ) -> None: """Initialize a new event.""" self.event_type = event_type @@ -624,7 +619,7 @@ class Event: # The only event type that shares context are the TIME_CHANGED return hash((self.event_type, self.context.id, self.time_fired)) - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: """Create a dict representation of this Event. Async friendly. @@ -639,7 +634,6 @@ class Event: def __repr__(self) -> str: """Return the representation.""" - # pylint: disable=maybe-no-member if self.data: return f"" @@ -662,11 +656,11 @@ class EventBus: def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners: Dict[str, List[Tuple[HassJob, Optional[Callable]]]] = {} + self._listeners: dict[str, list[tuple[HassJob, Callable | None]]] = {} self._hass = hass @callback - def async_listeners(self) -> Dict[str, int]: + def async_listeners(self) -> dict[str, int]: """Return dictionary with events and the number of listeners. This method must be run in the event loop. @@ -674,16 +668,16 @@ class EventBus: return {key: len(self._listeners[key]) for key in self._listeners} @property - def listeners(self) -> Dict[str, int]: + def listeners(self) -> dict[str, int]: """Return dictionary with events and the number of listeners.""" return run_callback_threadsafe(self._hass.loop, self.async_listeners).result() def fire( self, event_type: str, - event_data: Optional[Dict] = None, + event_data: dict | None = None, origin: EventOrigin = EventOrigin.local, - context: Optional[Context] = None, + context: Context | None = None, ) -> None: """Fire an event.""" self._hass.loop.call_soon_threadsafe( @@ -694,10 +688,10 @@ class EventBus: def async_fire( self, event_type: str, - event_data: Optional[Dict[str, Any]] = None, + event_data: dict[str, Any] | None = None, origin: EventOrigin = EventOrigin.local, - context: Optional[Context] = None, - time_fired: Optional[datetime.datetime] = None, + context: Context | None = None, + time_fired: datetime.datetime | None = None, ) -> None: """Fire an event. @@ -749,7 +743,7 @@ class EventBus: self, event_type: str, listener: Callable, - event_filter: Optional[Callable] = None, + event_filter: Callable | None = None, ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -770,7 +764,7 @@ class EventBus: @callback def _async_listen_filterable_job( - self, event_type: str, filterable_job: Tuple[HassJob, Optional[Callable]] + self, event_type: str, filterable_job: tuple[HassJob, Callable | None] ) -> CALLBACK_TYPE: self._listeners.setdefault(event_type, []).append(filterable_job) @@ -809,7 +803,7 @@ class EventBus: This method must be run in the event loop. """ - filterable_job: Optional[Tuple[HassJob, Optional[Callable]]] = None + filterable_job: tuple[HassJob, Callable | None] | None = None @callback def _onetime_listener(event: Event) -> None: @@ -833,7 +827,7 @@ class EventBus: @callback def _async_remove_listener( - self, event_type: str, filterable_job: Tuple[HassJob, Optional[Callable]] + self, event_type: str, filterable_job: tuple[HassJob, Callable | None] ) -> None: """Remove a listener of a specific event_type. @@ -882,11 +876,11 @@ class State: self, entity_id: str, state: str, - attributes: Optional[Mapping[str, Any]] = None, - last_changed: Optional[datetime.datetime] = None, - last_updated: Optional[datetime.datetime] = None, - context: Optional[Context] = None, - validate_entity_id: Optional[bool] = True, + attributes: Mapping[str, Any] | None = None, + last_changed: datetime.datetime | None = None, + last_updated: datetime.datetime | None = None, + context: Context | None = None, + validate_entity_id: bool | None = True, ) -> None: """Initialize a new state.""" state = str(state) @@ -910,7 +904,7 @@ class State: self.last_changed = last_changed or self.last_updated self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) - self._as_dict: Optional[Dict[str, Collection[Any]]] = None + self._as_dict: dict[str, Collection[Any]] | None = None @property def name(self) -> str: @@ -919,7 +913,7 @@ class State: "_", " " ) - def as_dict(self) -> Dict: + def as_dict(self) -> dict: """Return a dict representation of the State. Async friendly. @@ -944,7 +938,7 @@ class State: return self._as_dict @classmethod - def from_dict(cls, json_dict: Dict) -> Any: + def from_dict(cls, json_dict: dict) -> Any: """Initialize a state from a dict. Async friendly. @@ -1002,12 +996,12 @@ class StateMachine: def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None: """Initialize state machine.""" - self._states: Dict[str, State] = {} - self._reservations: Set[str] = set() + self._states: dict[str, State] = {} + self._reservations: set[str] = set() self._bus = bus self._loop = loop - def entity_ids(self, domain_filter: Optional[str] = None) -> List[str]: + def entity_ids(self, domain_filter: str | None = None) -> list[str]: """List of entity ids that are being tracked.""" future = run_callback_threadsafe( self._loop, self.async_entity_ids, domain_filter @@ -1016,8 +1010,8 @@ class StateMachine: @callback def async_entity_ids( - self, domain_filter: Optional[Union[str, Iterable]] = None - ) -> List[str]: + self, domain_filter: str | Iterable | None = None + ) -> list[str]: """List of entity ids that are being tracked. This method must be run in the event loop. @@ -1036,7 +1030,7 @@ class StateMachine: @callback def async_entity_ids_count( - self, domain_filter: Optional[Union[str, Iterable]] = None + self, domain_filter: str | Iterable | None = None ) -> int: """Count the entity ids that are being tracked. @@ -1052,16 +1046,14 @@ class StateMachine: [None for state in self._states.values() if state.domain in domain_filter] ) - def all(self, domain_filter: Optional[Union[str, Iterable]] = None) -> List[State]: + def all(self, domain_filter: str | Iterable | None = None) -> list[State]: """Create a list of all states.""" return run_callback_threadsafe( self._loop, self.async_all, domain_filter ).result() @callback - def async_all( - self, domain_filter: Optional[Union[str, Iterable]] = None - ) -> List[State]: + def async_all(self, domain_filter: str | Iterable | None = None) -> list[State]: """Create a list of all states matching the filter. This method must be run in the event loop. @@ -1076,7 +1068,7 @@ class StateMachine: state for state in self._states.values() if state.domain in domain_filter ] - def get(self, entity_id: str) -> Optional[State]: + def get(self, entity_id: str) -> State | None: """Retrieve state of entity_id or None if not found. Async friendly. @@ -1101,7 +1093,7 @@ class StateMachine: ).result() @callback - def async_remove(self, entity_id: str, context: Optional[Context] = None) -> bool: + def async_remove(self, entity_id: str, context: Context | None = None) -> bool: """Remove the state of an entity. Returns boolean to indicate if an entity was removed. @@ -1129,9 +1121,9 @@ class StateMachine: self, entity_id: str, new_state: str, - attributes: Optional[Mapping[str, Any]] = None, + attributes: Mapping[str, Any] | None = None, force_update: bool = False, - context: Optional[Context] = None, + context: Context | None = None, ) -> None: """Set the state of an entity, add entity if it does not exist. @@ -1178,9 +1170,9 @@ class StateMachine: self, entity_id: str, new_state: str, - attributes: Optional[Mapping[str, Any]] = None, + attributes: Mapping[str, Any] | None = None, force_update: bool = False, - context: Optional[Context] = None, + context: Context | None = None, ) -> None: """Set the state of an entity, add entity if it does not exist. @@ -1239,8 +1231,8 @@ class Service: def __init__( self, func: Callable, - schema: Optional[vol.Schema], - context: Optional[Context] = None, + schema: vol.Schema | None, + context: Context | None = None, ) -> None: """Initialize a service.""" self.job = HassJob(func) @@ -1256,8 +1248,8 @@ class ServiceCall: self, domain: str, service: str, - data: Optional[Dict] = None, - context: Optional[Context] = None, + data: dict | None = None, + context: Context | None = None, ) -> None: """Initialize a service call.""" self.domain = domain.lower() @@ -1281,16 +1273,16 @@ class ServiceRegistry: def __init__(self, hass: HomeAssistant) -> None: """Initialize a service registry.""" - self._services: Dict[str, Dict[str, Service]] = {} + self._services: dict[str, dict[str, Service]] = {} self._hass = hass @property - def services(self) -> Dict[str, Dict[str, Service]]: + def services(self) -> dict[str, dict[str, Service]]: """Return dictionary with per domain a list of available services.""" return run_callback_threadsafe(self._hass.loop, self.async_services).result() @callback - def async_services(self) -> Dict[str, Dict[str, Service]]: + def async_services(self) -> dict[str, dict[str, Service]]: """Return dictionary with per domain a list of available services. This method must be run in the event loop. @@ -1309,7 +1301,7 @@ class ServiceRegistry: domain: str, service: str, service_func: Callable, - schema: Optional[vol.Schema] = None, + schema: vol.Schema | None = None, ) -> None: """ Register a service. @@ -1326,7 +1318,7 @@ class ServiceRegistry: domain: str, service: str, service_func: Callable, - schema: Optional[vol.Schema] = None, + schema: vol.Schema | None = None, ) -> None: """ Register a service. @@ -1380,12 +1372,12 @@ class ServiceRegistry: self, domain: str, service: str, - service_data: Optional[Dict] = None, + service_data: dict | None = None, blocking: bool = False, - context: Optional[Context] = None, - limit: Optional[float] = SERVICE_CALL_LIMIT, - target: Optional[Dict] = None, - ) -> Optional[bool]: + context: Context | None = None, + limit: float | None = SERVICE_CALL_LIMIT, + target: dict | None = None, + ) -> bool | None: """ Call a service. @@ -1402,12 +1394,12 @@ class ServiceRegistry: self, domain: str, service: str, - service_data: Optional[Dict] = None, + service_data: dict | None = None, blocking: bool = False, - context: Optional[Context] = None, - limit: Optional[float] = SERVICE_CALL_LIMIT, - target: Optional[Dict] = None, - ) -> Optional[bool]: + context: Context | None = None, + limit: float | None = SERVICE_CALL_LIMIT, + target: dict | None = None, + ) -> bool | None: """ Call a service. @@ -1495,7 +1487,7 @@ class ServiceRegistry: return False def _run_service_in_background( - self, coro_or_task: Union[Coroutine, asyncio.Task], service_call: ServiceCall + self, coro_or_task: Coroutine | asyncio.Task, service_call: ServiceCall ) -> None: """Run service call in background, catching and logging any exceptions.""" @@ -1540,8 +1532,8 @@ class Config: self.location_name: str = "Home" self.time_zone: datetime.tzinfo = dt_util.UTC self.units: UnitSystem = METRIC_SYSTEM - self.internal_url: Optional[str] = None - self.external_url: Optional[str] = None + self.internal_url: str | None = None + self.external_url: str | None = None self.config_source: str = "default" @@ -1549,22 +1541,22 @@ class Config: self.skip_pip: bool = False # List of loaded components - self.components: Set[str] = set() + self.components: set[str] = set() # API (HTTP) server configuration, see components.http.ApiConfig - self.api: Optional[Any] = None + self.api: Any | None = None # Directory that holds the configuration - self.config_dir: Optional[str] = None + self.config_dir: str | None = None # List of allowed external dirs to access - self.allowlist_external_dirs: Set[str] = set() + self.allowlist_external_dirs: set[str] = set() # List of allowed external URLs that integrations may use - self.allowlist_external_urls: Set[str] = set() + self.allowlist_external_urls: set[str] = set() # Dictionary of Media folders that integrations may use - self.media_dirs: Dict[str, str] = {} + self.media_dirs: dict[str, str] = {} # If Home Assistant is running in safe mode self.safe_mode: bool = False @@ -1572,7 +1564,7 @@ class Config: # Use legacy template behavior self.legacy_templates: bool = False - def distance(self, lat: float, lon: float) -> Optional[float]: + def distance(self, lat: float, lon: float) -> float | None: """Calculate distance from Home Assistant. Async friendly. @@ -1623,7 +1615,7 @@ class Config: return False - def as_dict(self) -> Dict: + def as_dict(self) -> dict: """Create a dictionary representation of the configuration. Async friendly. @@ -1668,15 +1660,15 @@ class Config: self, *, source: str, - latitude: Optional[float] = None, - longitude: Optional[float] = None, - elevation: Optional[int] = None, - unit_system: Optional[str] = None, - location_name: Optional[str] = None, - time_zone: Optional[str] = None, + latitude: float | None = None, + longitude: float | None = None, + elevation: int | None = None, + unit_system: str | None = None, + location_name: str | None = None, + time_zone: str | None = None, # pylint: disable=dangerous-default-value # _UNDEFs not modified - external_url: Optional[Union[str, dict]] = _UNDEF, - internal_url: Optional[Union[str, dict]] = _UNDEF, + external_url: str | dict | None = _UNDEF, + internal_url: str | dict | None = _UNDEF, ) -> None: """Update the configuration from a dictionary.""" self.config_source = source diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e8235c9a23c..40c9ace0f8d 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import asyncio from types import MappingProxyType -from typing import Any, Dict, List, Optional +from typing import Any import uuid import voluptuous as vol @@ -43,7 +43,7 @@ class UnknownStep(FlowError): class AbortFlow(FlowError): """Exception to indicate a flow needs to be aborted.""" - def __init__(self, reason: str, description_placeholders: Optional[Dict] = None): + def __init__(self, reason: str, description_placeholders: dict | None = None): """Initialize an abort flow exception.""" super().__init__(f"Flow aborted: {reason}") self.reason = reason @@ -59,8 +59,8 @@ class FlowManager(abc.ABC): ) -> None: """Initialize the flow manager.""" self.hass = hass - self._initializing: Dict[str, List[asyncio.Future]] = {} - self._progress: Dict[str, Any] = {} + self._initializing: dict[str, list[asyncio.Future]] = {} + self._progress: dict[str, Any] = {} async def async_wait_init_flow_finish(self, handler: str) -> None: """Wait till all flows in progress are initialized.""" @@ -76,8 +76,8 @@ class FlowManager(abc.ABC): self, handler_key: Any, *, - context: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> FlowHandler: """Create a flow for specified handler. @@ -86,31 +86,29 @@ class FlowManager(abc.ABC): @abc.abstractmethod async def async_finish_flow( - self, flow: "FlowHandler", result: Dict[str, Any] - ) -> Dict[str, Any]: + self, flow: FlowHandler, result: dict[str, Any] + ) -> dict[str, Any]: """Finish a config flow and add an entry.""" - async def async_post_init( - self, flow: "FlowHandler", result: Dict[str, Any] - ) -> None: + async def async_post_init(self, flow: FlowHandler, result: dict[str, Any]) -> None: """Entry has finished executing its first step asynchronously.""" @callback - def async_progress(self) -> List[Dict]: + def async_progress(self, include_uninitialized: bool = False) -> list[dict]: """Return the flows in progress.""" return [ { "flow_id": flow.flow_id, "handler": flow.handler, "context": flow.context, - "step_id": flow.cur_step["step_id"], + "step_id": flow.cur_step["step_id"] if flow.cur_step else None, } for flow in self._progress.values() - if flow.cur_step is not None + if include_uninitialized or flow.cur_step is not None ] async def async_init( - self, handler: str, *, context: Optional[Dict] = None, data: Any = None + self, handler: str, *, context: dict | None = None, data: Any = None ) -> Any: """Start a configuration flow.""" if context is None: @@ -142,7 +140,7 @@ class FlowManager(abc.ABC): return result async def async_configure( - self, flow_id: str, user_input: Optional[Dict] = None + self, flow_id: str, user_input: dict | None = None ) -> Any: """Continue a configuration flow.""" flow = self._progress.get(flow_id) @@ -198,9 +196,9 @@ class FlowManager(abc.ABC): self, flow: Any, step_id: str, - user_input: Optional[Dict], - step_done: Optional[asyncio.Future] = None, - ) -> Dict: + user_input: dict | None, + step_done: asyncio.Future | None = None, + ) -> dict: """Handle a step of a flow.""" method = f"async_step_{step_id}" @@ -213,7 +211,7 @@ class FlowManager(abc.ABC): ) try: - result: Dict = await getattr(flow, method)(user_input) + result: dict = await getattr(flow, method)(user_input) except AbortFlow as err: result = _create_abort_data( flow.flow_id, flow.handler, err.reason, err.description_placeholders @@ -265,14 +263,13 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - cur_step: Optional[Dict[str, str]] = None - # Ignore types, pylint workaround: https://github.com/PyCQA/pylint/issues/3167 + cur_step: dict[str, str] | None = None + # Ignore types: https://github.com/PyCQA/pylint/issues/3167 flow_id: str = None # type: ignore hass: HomeAssistant = None # type: ignore handler: str = None # type: ignore - # Pylint workaround: https://github.com/PyCQA/pylint/issues/3167 # Ensure the attribute has a subscriptable, but immutable, default value. - context: Dict = MappingProxyType({}) # type: ignore + context: dict = MappingProxyType({}) # type: ignore # Set by _async_create_flow callback init_step = "init" @@ -281,7 +278,7 @@ class FlowHandler: VERSION = 1 @property - def source(self) -> Optional[str]: + def source(self) -> str | None: """Source that initialized the flow.""" if not hasattr(self, "context"): return None @@ -302,9 +299,9 @@ class FlowHandler: *, step_id: str, data_schema: vol.Schema = None, - errors: Optional[Dict] = None, - description_placeholders: Optional[Dict] = None, - ) -> Dict[str, Any]: + errors: dict | None = None, + description_placeholders: dict | None = None, + ) -> dict[str, Any]: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -321,10 +318,10 @@ class FlowHandler: self, *, title: str, - data: Dict, - description: Optional[str] = None, - description_placeholders: Optional[Dict] = None, - ) -> Dict[str, Any]: + data: dict, + description: str | None = None, + description_placeholders: dict | None = None, + ) -> dict[str, Any]: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -339,8 +336,8 @@ class FlowHandler: @callback def async_abort( - self, *, reason: str, description_placeholders: Optional[Dict] = None - ) -> Dict[str, Any]: + self, *, reason: str, description_placeholders: dict | None = None + ) -> dict[str, Any]: """Abort the config flow.""" return _create_abort_data( self.flow_id, self.handler, reason, description_placeholders @@ -348,8 +345,8 @@ class FlowHandler: @callback def async_external_step( - self, *, step_id: str, url: str, description_placeholders: Optional[Dict] = None - ) -> Dict[str, Any]: + self, *, step_id: str, url: str, description_placeholders: dict | None = None + ) -> dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -361,7 +358,7 @@ class FlowHandler: } @callback - def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]: + def async_external_step_done(self, *, next_step_id: str) -> dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, @@ -376,8 +373,8 @@ class FlowHandler: *, step_id: str, progress_action: str, - description_placeholders: Optional[Dict] = None, - ) -> Dict[str, Any]: + description_placeholders: dict | None = None, + ) -> dict[str, Any]: """Show a progress message to the user, without user input allowed.""" return { "type": RESULT_TYPE_SHOW_PROGRESS, @@ -389,7 +386,7 @@ class FlowHandler: } @callback - def async_show_progress_done(self, *, next_step_id: str) -> Dict[str, Any]: + def async_show_progress_done(self, *, next_step_id: str) -> dict[str, Any]: """Mark the progress done.""" return { "type": RESULT_TYPE_SHOW_PROGRESS_DONE, @@ -404,8 +401,8 @@ def _create_abort_data( flow_id: str, handler: str, reason: str, - description_placeholders: Optional[Dict] = None, -) -> Dict[str, Any]: + description_placeholders: dict | None = None, +) -> dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_ABORT, diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 84ba2cfa348..375db789618 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,10 +1,12 @@ """The exceptions used by Home Assistant.""" -from typing import TYPE_CHECKING, Generator, Optional, Sequence +from __future__ import annotations + +from typing import TYPE_CHECKING, Generator, Sequence import attr if TYPE_CHECKING: - from .core import Context # noqa: F401 pylint: disable=unused-import + from .core import Context class HomeAssistantError(Exception): @@ -113,12 +115,12 @@ class Unauthorized(HomeAssistantError): def __init__( self, - context: Optional["Context"] = None, - user_id: Optional[str] = None, - entity_id: Optional[str] = None, - config_entry_id: Optional[str] = None, - perm_category: Optional[str] = None, - permission: Optional[str] = None, + context: Context | None = None, + user_id: str | None = None, + entity_id: str | None = None, + config_entry_id: str | None = None, + perm_category: str | None = None, + permission: str | None = None, ) -> None: """Unauthorized error.""" super().__init__(self.__class__.__name__) diff --git a/homeassistant/generated/__init__.py b/homeassistant/generated/__init__.py new file mode 100644 index 00000000000..b86c779f9b8 --- /dev/null +++ b/homeassistant/generated/__init__.py @@ -0,0 +1,4 @@ +"""All files in this module are automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c3d629ebe29..d66736b2b3a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -86,15 +86,16 @@ FLOWS = [ "gogogate2", "gpslogger", "gree", - "griddy", "guardian", "habitica", "hangouts", "harmony", "heos", "hisense_aehw4a1", + "hive", "hlk_sw16", "home_connect", + "home_plus_control", "homekit", "homekit_controller", "homematicip_cloud", @@ -195,6 +196,7 @@ FLOWS = [ "rpi_power", "ruckus_unleashed", "samsungtv", + "screenlogic", "sense", "sentry", "sharkiq", @@ -247,6 +249,7 @@ FLOWS = [ "upnp", "velbus", "vera", + "verisure", "vesync", "vilfo", "vizio", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 31ee42bc48c..83622545551 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -16,6 +16,11 @@ DHCP = [ "hostname": "connect", "macaddress": "B8B7F1*" }, + { + "domain": "august", + "hostname": "august*", + "macaddress": "E076D0*" + }, { "domain": "axis", "hostname": "axis-00408c*", @@ -31,6 +36,27 @@ DHCP = [ "hostname": "axis-b8a44f*", "macaddress": "B8A44F*" }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "B85F98*" + }, + { + "domain": "broadlink", + "macaddress": "34EA34*" + }, + { + "domain": "broadlink", + "macaddress": "24DFA7*" + }, + { + "domain": "broadlink", + "macaddress": "A043B0*" + }, + { + "domain": "broadlink", + "macaddress": "B4430D*" + }, { "domain": "flume", "hostname": "flume-gw-*", @@ -109,6 +135,16 @@ DHCP = [ "hostname": "irobot-*", "macaddress": "501479*" }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "80A589*" + }, + { + "domain": "screenlogic", + "hostname": "pentair: *", + "macaddress": "00C033*" + }, { "domain": "sense", "hostname": "sense-*", @@ -153,5 +189,9 @@ DHCP = [ "domain": "toon", "hostname": "eneco-*", "macaddress": "74C63B*" + }, + { + "domain": "verisure", + "macaddress": "0023C1*" } ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 5521ab9da8f..a6af4d93fb8 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -57,6 +57,15 @@ ZEROCONF = { "_esphomelib._tcp.local.": [ { "domain": "esphome" + }, + { + "domain": "zha", + "name": "tube*" + } + ], + "_fbx-api._tcp.local.": [ + { + "domain": "freebox" } ], "_googlecast._tcp.local.": [ diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 6f22be8d323..a1964c432fc 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,6 +1,8 @@ """Helper methods for components within Home Assistant.""" +from __future__ import annotations + import re -from typing import TYPE_CHECKING, Any, Iterable, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Iterable, Sequence from homeassistant.const import CONF_PLATFORM @@ -8,7 +10,7 @@ if TYPE_CHECKING: from .typing import ConfigType -def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any, Any]]: +def config_per_platform(config: ConfigType, domain: str) -> Iterable[tuple[Any, Any]]: """Break a component config into different platforms. For example, will find 'switch', 'switch 2', 'switch 3', .. etc @@ -32,7 +34,7 @@ def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any yield platform, item -def extract_domain_configs(config: "ConfigType", domain: str) -> Sequence[str]: +def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: """Extract keys from config for given domain name. Async friendly. diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 3e1e45e5981..f3ded75062e 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,8 +1,11 @@ """Helper for aiohttp webclient stuff.""" +from __future__ import annotations + import asyncio +from contextlib import suppress from ssl import SSLContext import sys -from typing import Any, Awaitable, Optional, Union, cast +from typing import Any, Awaitable, cast import aiohttp from aiohttp import web @@ -11,9 +14,8 @@ from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.frame import warn_use -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util @@ -29,16 +31,15 @@ SERVER_SOFTWARE = "HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}".format( @callback @bind_hass def async_get_clientsession( - hass: HomeAssistantType, verify_ssl: bool = True + hass: HomeAssistant, verify_ssl: bool = True ) -> aiohttp.ClientSession: """Return default aiohttp ClientSession. This method must be run in the event loop. """ + key = DATA_CLIENTSESSION_NOTVERIFY if verify_ssl: key = DATA_CLIENTSESSION - else: - key = DATA_CLIENTSESSION_NOTVERIFY if key not in hass.data: hass.data[key] = async_create_clientsession(hass, verify_ssl) @@ -49,7 +50,7 @@ def async_get_clientsession( @callback @bind_hass def async_create_clientsession( - hass: HomeAssistantType, + hass: HomeAssistant, verify_ssl: bool = True, auto_cleanup: bool = True, **kwargs: Any, @@ -82,12 +83,12 @@ def async_create_clientsession( @bind_hass async def async_aiohttp_proxy_web( - hass: HomeAssistantType, + hass: HomeAssistant, request: web.BaseRequest, web_coro: Awaitable[aiohttp.ClientResponse], buffer_size: int = 102400, timeout: int = 10, -) -> Optional[web.StreamResponse]: +) -> web.StreamResponse | None: """Stream websession request to aiohttp web response.""" try: with async_timeout.timeout(timeout): @@ -115,10 +116,10 @@ async def async_aiohttp_proxy_web( @bind_hass async def async_aiohttp_proxy_stream( - hass: HomeAssistantType, + hass: HomeAssistant, request: web.BaseRequest, stream: aiohttp.StreamReader, - content_type: Optional[str], + content_type: str | None, buffer_size: int = 102400, timeout: int = 10, ) -> web.StreamResponse: @@ -128,7 +129,8 @@ async def async_aiohttp_proxy_stream( response.content_type = content_type await response.prepare(request) - try: + # Suppressing something went wrong fetching data, closed connection + with suppress(asyncio.TimeoutError, aiohttp.ClientError): while hass.is_running: with async_timeout.timeout(timeout): data = await stream.read(buffer_size) @@ -137,16 +139,12 @@ async def async_aiohttp_proxy_stream( break await response.write(data) - except (asyncio.TimeoutError, aiohttp.ClientError): - # Something went wrong fetching data, closed connection - pass - return response @callback def _async_register_clientsession_shutdown( - hass: HomeAssistantType, clientsession: aiohttp.ClientSession + hass: HomeAssistant, clientsession: aiohttp.ClientSession ) -> None: """Register ClientSession close on Home Assistant shutdown. @@ -163,7 +161,7 @@ def _async_register_clientsession_shutdown( @callback def _async_get_connector( - hass: HomeAssistantType, verify_ssl: bool = True + hass: HomeAssistant, verify_ssl: bool = True ) -> aiohttp.BaseConnector: """Return the connector pool for aiohttp. @@ -175,7 +173,7 @@ def _async_get_connector( return cast(aiohttp.BaseConnector, hass.data[key]) if verify_ssl: - ssl_context: Union[bool, SSLContext] = ssl_util.client_context() + ssl_context: bool | SSLContext = ssl_util.client_context() else: ssl_context = False diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 164207a8b2a..af568b40418 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,16 +1,16 @@ """Provide a way to connect devices to one physical location.""" +from __future__ import annotations + from collections import OrderedDict -from typing import Container, Dict, Iterable, List, MutableMapping, Optional, cast +from typing import Container, Iterable, MutableMapping, cast import attr -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.loader import bind_hass from homeassistant.util import slugify -from .typing import HomeAssistantType - # mypy: disallow-any-generics DATA_REGISTRY = "area_registry" @@ -26,7 +26,7 @@ class AreaEntry: name: str = attr.ib() normalized_name: str = attr.ib() - id: Optional[str] = attr.ib(default=None) + id: str | None = attr.ib(default=None) def generate_id(self, existing_ids: Container[str]) -> None: """Initialize ID.""" @@ -41,20 +41,20 @@ class AreaEntry: class AreaRegistry: """Class to hold a registry of areas.""" - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the area registry.""" self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - self._normalized_name_area_idx: Dict[str, str] = {} + self._normalized_name_area_idx: dict[str, str] = {} @callback - def async_get_area(self, area_id: str) -> Optional[AreaEntry]: + def async_get_area(self, area_id: str) -> AreaEntry | None: """Get area by id.""" return self.areas.get(area_id) @callback - def async_get_area_by_name(self, name: str) -> Optional[AreaEntry]: + def async_get_area_by_name(self, name: str) -> AreaEntry | None: """Get area by name.""" normalized_name = normalize_area_name(name) if normalized_name not in self._normalized_name_area_idx: @@ -132,11 +132,8 @@ class AreaRegistry: normalized_name = normalize_area_name(name) - if normalized_name != old.normalized_name: - if self.async_get_area_by_name(name): - raise ValueError( - f"The name {name} ({normalized_name}) is already in use" - ) + if normalized_name != old.normalized_name and self.async_get_area_by_name(name): + raise ValueError(f"The name {name} ({normalized_name}) is already in use") changes["name"] = name changes["normalized_name"] = normalized_name @@ -171,7 +168,7 @@ class AreaRegistry: self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self) -> Dict[str, List[Dict[str, Optional[str]]]]: + def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: """Return data of area registry to store in a file.""" data = {} @@ -187,12 +184,12 @@ class AreaRegistry: @callback -def async_get(hass: HomeAssistantType) -> AreaRegistry: +def async_get(hass: HomeAssistant) -> AreaRegistry: """Get area registry.""" return cast(AreaRegistry, hass.data[DATA_REGISTRY]) -async def async_load(hass: HomeAssistantType) -> None: +async def async_load(hass: HomeAssistant) -> None: """Load area registry.""" assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = AreaRegistry(hass) @@ -200,7 +197,7 @@ async def async_load(hass: HomeAssistantType) -> None: @bind_hass -async def async_get_registry(hass: HomeAssistantType) -> AreaRegistry: +async def async_get_registry(hass: HomeAssistant) -> AreaRegistry: """Get area registry. This is deprecated and will be removed in the future. Use async_get instead. diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 7b7b53d3c0f..a486c8bcc14 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -4,7 +4,8 @@ from __future__ import annotations from collections import OrderedDict import logging import os -from typing import List, NamedTuple, Optional +from pathlib import Path +from typing import NamedTuple import voluptuous as vol @@ -34,8 +35,8 @@ class CheckConfigError(NamedTuple): """Configuration check error.""" message: str - domain: Optional[str] - config: Optional[ConfigType] + domain: str | None + config: ConfigType | None class HomeAssistantConfig(OrderedDict): @@ -44,13 +45,13 @@ class HomeAssistantConfig(OrderedDict): def __init__(self) -> None: """Initialize HA config.""" super().__init__() - self.errors: List[CheckConfigError] = [] + self.errors: list[CheckConfigError] = [] def add_error( self, message: str, - domain: Optional[str] = None, - config: Optional[ConfigType] = None, + domain: str | None = None, + config: ConfigType | None = None, ) -> HomeAssistantConfig: """Add a single error.""" self.errors.append(CheckConfigError(str(message), domain, config)) @@ -87,13 +88,18 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig try: if not await hass.async_add_executor_job(os.path.isfile, config_path): return result.add_error("File configuration.yaml not found.") - config = await hass.async_add_executor_job(load_yaml_config_file, config_path) + + assert hass.config.config_dir is not None + + config = await hass.async_add_executor_job( + load_yaml_config_file, + config_path, + yaml_loader.Secrets(Path(hass.config.config_dir)), + ) except FileNotFoundError: return result.add_error(f"File not found: {config_path}") except HomeAssistantError as err: return result.add_error(f"Error loading {config_path}: {err}") - finally: - yaml_loader.clear_secret_cache() # Extract and validate core [homeassistant] config try: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index abeef0f0d68..6185b74068d 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -1,9 +1,11 @@ """Helper to deal with YAML + storage.""" +from __future__ import annotations + from abc import ABC, abstractmethod import asyncio from dataclasses import dataclass import logging -from typing import Any, Awaitable, Callable, Dict, Iterable, List, Optional, cast +from typing import Any, Awaitable, Callable, Iterable, Optional, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -16,7 +18,6 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify STORAGE_VERSION = 1 @@ -72,9 +73,9 @@ class IDManager: def __init__(self) -> None: """Initiate the ID manager.""" - self.collections: List[Dict[str, Any]] = [] + self.collections: list[dict[str, Any]] = [] - def add_collection(self, collection: Dict[str, Any]) -> None: + def add_collection(self, collection: dict[str, Any]) -> None: """Add a collection to check for ID usage.""" self.collections.append(collection) @@ -98,17 +99,17 @@ class IDManager: class ObservableCollection(ABC): """Base collection type that can be observed.""" - def __init__(self, logger: logging.Logger, id_manager: Optional[IDManager] = None): + def __init__(self, logger: logging.Logger, id_manager: IDManager | None = None): """Initialize the base collection.""" self.logger = logger self.id_manager = id_manager or IDManager() - self.data: Dict[str, dict] = {} - self.listeners: List[ChangeListener] = [] + self.data: dict[str, dict] = {} + self.listeners: list[ChangeListener] = [] self.id_manager.add_collection(self.data) @callback - def async_items(self) -> List[dict]: + def async_items(self) -> list[dict]: """Return list of items in collection.""" return list(self.data.values()) @@ -134,7 +135,7 @@ class ObservableCollection(ABC): class YamlCollection(ObservableCollection): """Offer a collection based on static data.""" - async def async_load(self, data: List[dict]) -> None: + async def async_load(self, data: list[dict]) -> None: """Load the YAML collection. Overrides existing data.""" old_ids = set(self.data) @@ -171,7 +172,7 @@ class StorageCollection(ObservableCollection): self, store: Store, logger: logging.Logger, - id_manager: Optional[IDManager] = None, + id_manager: IDManager | None = None, ): """Initialize the storage collection.""" super().__init__(logger, id_manager) @@ -182,7 +183,7 @@ class StorageCollection(ObservableCollection): """Home Assistant object.""" return self.store.hass - async def _async_load_data(self) -> Optional[dict]: + async def _async_load_data(self) -> dict | None: """Load the data.""" return cast(Optional[dict], await self.store.async_load()) @@ -274,7 +275,7 @@ class IDLessCollection(ObservableCollection): counter = 0 - async def async_load(self, data: List[dict]) -> None: + async def async_load(self, data: list[dict]) -> None: """Load the collection. Overrides existing data.""" await self.notify_changes( [ @@ -301,7 +302,7 @@ class IDLessCollection(ObservableCollection): @callback def sync_entity_lifecycle( - hass: HomeAssistantType, + hass: HomeAssistant, domain: str, platform: str, entity_component: EntityComponent, diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index c592681a015..6adcb4d1fd9 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -1,12 +1,15 @@ """Offer reusable conditions.""" +from __future__ import annotations + import asyncio from collections import deque +from contextlib import contextmanager from datetime import datetime, timedelta import functools as ft import logging import re import sys -from typing import Any, Callable, Container, List, Optional, Set, Union, cast +from typing import Any, Callable, Container, Generator, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( @@ -51,6 +54,17 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util +from .trace import ( + TraceElement, + trace_append_element, + trace_path, + trace_path_get, + trace_stack_cv, + trace_stack_pop, + trace_stack_push, + trace_stack_top, +) + FROM_CONFIG_FORMAT = "{}_from_config" ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" @@ -63,9 +77,56 @@ INPUT_ENTITY_ID = re.compile( ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] +def condition_trace_append(variables: TemplateVarsType, path: str) -> TraceElement: + """Append a TraceElement to trace[path].""" + trace_element = TraceElement(variables, path) + trace_append_element(trace_element) + return trace_element + + +def condition_trace_set_result(result: bool, **kwargs: Any) -> None: + """Set the result of TraceElement at the top of the stack.""" + node = trace_stack_top(trace_stack_cv) + + # The condition function may be called directly, in which case tracing + # is not setup + if not node: + return + + node.set_result(result=result, **kwargs) + + +@contextmanager +def trace_condition(variables: TemplateVarsType) -> Generator: + """Trace condition evaluation.""" + trace_element = condition_trace_append(variables, trace_path_get()) + trace_stack_push(trace_stack_cv, trace_element) + try: + yield trace_element + except Exception as ex: + trace_element.set_error(ex) + raise ex + finally: + trace_stack_pop(trace_stack_cv) + + +def trace_condition_function(condition: ConditionCheckerType) -> ConditionCheckerType: + """Wrap a condition function to enable basic tracing.""" + + @ft.wraps(condition) + def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + """Trace condition.""" + with trace_condition(variables): + result = condition(hass, variables) + condition_trace_set_result(result) + return result + + return wrapper + + async def async_from_config( hass: HomeAssistant, - config: Union[ConfigType, Template], + config: ConfigType | Template, config_validation: bool = True, ) -> ConditionCheckerType: """Turn a condition configuration into a method. @@ -111,6 +172,7 @@ async def async_and_from_config( await async_from_config(hass, entry, False) for entry in config["conditions"] ] + @trace_condition_function def if_and_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: @@ -118,8 +180,9 @@ async def async_and_from_config( errors = [] for index, check in enumerate(checks): try: - if not check(hass, variables): - return False + with trace_path(["conditions", str(index)]): + if not check(hass, variables): + return False except ConditionError as ex: errors.append( ConditionErrorIndex("and", index=index, total=len(checks), error=ex) @@ -144,6 +207,7 @@ async def async_or_from_config( await async_from_config(hass, entry, False) for entry in config["conditions"] ] + @trace_condition_function def if_or_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: @@ -151,8 +215,9 @@ async def async_or_from_config( errors = [] for index, check in enumerate(checks): try: - if check(hass, variables): - return True + with trace_path(["conditions", str(index)]): + if check(hass, variables): + return True except ConditionError as ex: errors.append( ConditionErrorIndex("or", index=index, total=len(checks), error=ex) @@ -177,6 +242,7 @@ async def async_not_from_config( await async_from_config(hass, entry, False) for entry in config["conditions"] ] + @trace_condition_function def if_not_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: @@ -184,8 +250,9 @@ async def async_not_from_config( errors = [] for index, check in enumerate(checks): try: - if check(hass, variables): - return False + with trace_path(["conditions", str(index)]): + if check(hass, variables): + return False except ConditionError as ex: errors.append( ConditionErrorIndex("not", index=index, total=len(checks), error=ex) @@ -202,10 +269,10 @@ async def async_not_from_config( def numeric_state( hass: HomeAssistant, - entity: Union[None, str, State], - below: Optional[Union[float, str]] = None, - above: Optional[Union[float, str]] = None, - value_template: Optional[Template] = None, + entity: None | str | State, + below: float | str | None = None, + above: float | str | None = None, + value_template: Template | None = None, variables: TemplateVarsType = None, ) -> bool: """Test a numeric state condition.""" @@ -223,12 +290,12 @@ def numeric_state( def async_numeric_state( hass: HomeAssistant, - entity: Union[None, str, State], - below: Optional[Union[float, str]] = None, - above: Optional[Union[float, str]] = None, - value_template: Optional[Template] = None, + entity: None | str | State, + below: float | str | None = None, + above: float | str | None = None, + value_template: Template | None = None, variables: TemplateVarsType = None, - attribute: Optional[str] = None, + attribute: str | None = None, ) -> bool: """Test a numeric state condition.""" if entity is None: @@ -291,6 +358,11 @@ def async_numeric_state( return False try: if fvalue >= float(below_entity.state): + condition_trace_set_result( + False, + state=fvalue, + wanted_state_below=float(below_entity.state), + ) return False except (ValueError, TypeError) as ex: raise ConditionErrorMessage( @@ -298,6 +370,7 @@ def async_numeric_state( f"the 'below' entity {below} state '{below_entity.state}' cannot be processed as a number", ) from ex elif fvalue >= below: + condition_trace_set_result(False, state=fvalue, wanted_state_below=below) return False if above is not None: @@ -314,6 +387,11 @@ def async_numeric_state( return False try: if fvalue <= float(above_entity.state): + condition_trace_set_result( + False, + state=fvalue, + wanted_state_above=float(above_entity.state), + ) return False except (ValueError, TypeError) as ex: raise ConditionErrorMessage( @@ -321,8 +399,10 @@ def async_numeric_state( f"the 'above' entity {above} state '{above_entity.state}' cannot be processed as a number", ) from ex elif fvalue <= above: + condition_trace_set_result(False, state=fvalue, wanted_state_above=above) return False + condition_trace_set_result(True, state=fvalue) return True @@ -338,6 +418,7 @@ def async_numeric_state_from_config( above = config.get(CONF_ABOVE) value_template = config.get(CONF_VALUE_TEMPLATE) + @trace_condition_function def if_numeric_state( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: @@ -348,10 +429,17 @@ def async_numeric_state_from_config( errors = [] for index, entity_id in enumerate(entity_ids): try: - if not async_numeric_state( - hass, entity_id, below, above, value_template, variables, attribute - ): - return False + with trace_path(["entity_id", str(index)]), trace_condition(variables): + if not async_numeric_state( + hass, + entity_id, + below, + above, + value_template, + variables, + attribute, + ): + return False except ConditionError as ex: errors.append( ConditionErrorIndex( @@ -370,10 +458,10 @@ def async_numeric_state_from_config( def state( hass: HomeAssistant, - entity: Union[None, str, State], + entity: None | str | State, req_state: Any, - for_period: Optional[timedelta] = None, - attribute: Optional[str] = None, + for_period: timedelta | None = None, + attribute: str | None = None, ) -> bool: """Test if state matches requirements. @@ -424,9 +512,13 @@ def state( break if for_period is None or not is_state: + condition_trace_set_result(is_state, state=value, wanted_state=state_value) return is_state - return dt_util.utcnow() - for_period > entity.last_changed + duration = dt_util.utcnow() - for_period + duration_ok = duration > entity.last_changed + condition_trace_set_result(duration_ok, state=value, duration=duration) + return duration_ok def state_from_config( @@ -436,20 +528,22 @@ def state_from_config( if config_validation: config = cv.STATE_CONDITION_SCHEMA(config) entity_ids = config.get(CONF_ENTITY_ID, []) - req_states: Union[str, List[str]] = config.get(CONF_STATE, []) + req_states: str | list[str] = config.get(CONF_STATE, []) for_period = config.get("for") attribute = config.get(CONF_ATTRIBUTE) if not isinstance(req_states, list): req_states = [req_states] + @trace_condition_function def if_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" errors = [] for index, entity_id in enumerate(entity_ids): try: - if not state(hass, entity_id, req_states, for_period, attribute): - return False + with trace_path(["entity_id", str(index)]), trace_condition(variables): + if not state(hass, entity_id, req_states, for_period, attribute): + return False except ConditionError as ex: errors.append( ConditionErrorIndex( @@ -468,10 +562,10 @@ def state_from_config( def sun( hass: HomeAssistant, - before: Optional[str] = None, - after: Optional[str] = None, - before_offset: Optional[timedelta] = None, - after_offset: Optional[timedelta] = None, + before: str | None = None, + after: str | None = None, + before_offset: timedelta | None = None, + after_offset: timedelta | None = None, ) -> bool: """Test if current time matches sun requirements.""" utcnow = dt_util.utcnow() @@ -532,11 +626,12 @@ def sun_from_config( before_offset = config.get("before_offset") after_offset = config.get("after_offset") - def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + @trace_condition_function + def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Validate time based if-condition.""" return sun(hass, before, after, before_offset, after_offset) - return time_if + return sun_if def template( @@ -568,6 +663,7 @@ def async_template_from_config( config = cv.TEMPLATE_CONDITION_SCHEMA(config) value_template = cast(Template, config.get(CONF_VALUE_TEMPLATE)) + @trace_condition_function def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Validate template based if-condition.""" value_template.hass = hass @@ -579,9 +675,9 @@ def async_template_from_config( def time( hass: HomeAssistant, - before: Optional[Union[dt_util.dt.time, str]] = None, - after: Optional[Union[dt_util.dt.time, str]] = None, - weekday: Union[None, str, Container[str]] = None, + before: dt_util.dt.time | str | None = None, + after: dt_util.dt.time | str | None = None, + weekday: None | str | Container[str] = None, ) -> bool: """Test if local time condition matches. @@ -648,6 +744,7 @@ def time_from_config( after = config.get(CONF_AFTER) weekday = config.get(CONF_WEEKDAY) + @trace_condition_function def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Validate time based if-condition.""" return time(hass, before, after, weekday) @@ -657,8 +754,8 @@ def time_from_config( def zone( hass: HomeAssistant, - zone_ent: Union[None, str, State], - entity: Union[None, str, State], + zone_ent: None | str | State, + entity: None | str | State, ) -> bool: """Test if zone-condition matches. @@ -713,6 +810,7 @@ def zone_from_config( entity_ids = config.get(CONF_ENTITY_ID, []) zone_entity_ids = config.get(CONF_ZONE, []) + @trace_condition_function def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" errors = [] @@ -753,15 +851,17 @@ async def async_device_from_config( platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "condition" ) - return cast( - ConditionCheckerType, - platform.async_condition_from_config(config, config_validation), # type: ignore + return trace_condition_function( + cast( + ConditionCheckerType, + platform.async_condition_from_config(config, config_validation), # type: ignore + ) ) async def async_validate_condition_config( - hass: HomeAssistant, config: Union[ConfigType, Template] -) -> Union[ConfigType, Template]: + hass: HomeAssistant, config: ConfigType | Template +) -> ConfigType | Template: """Validate config.""" if isinstance(config, Template): return config @@ -786,9 +886,9 @@ async def async_validate_condition_config( @callback -def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]: +def async_extract_entities(config: ConfigType | Template) -> set[str]: """Extract entities from a condition.""" - referenced: Set[str] = set() + referenced: set[str] = set() to_process = deque([config]) while to_process: @@ -814,7 +914,7 @@ def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]: @callback -def async_extract_devices(config: Union[ConfigType, Template]) -> Set[str]: +def async_extract_devices(config: ConfigType | Template) -> set[str]: """Extract devices from a condition.""" referenced = set() to_process = deque([config]) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 889981537c6..6abcf0ece56 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,9 +1,10 @@ """Helpers for data entry flows for config entries.""" -from typing import Any, Awaitable, Callable, Dict, Optional, Union +from __future__ import annotations + +from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries - -from .typing import HomeAssistantType +from homeassistant.core import HomeAssistant DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] @@ -27,8 +28,8 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -38,10 +39,11 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return await self.async_step_confirm() async def async_step_confirm( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Confirm setup.""" if user_input is None: + self._set_confirm_only() return self.async_show_form(step_id="confirm") if self.source == config_entries.SOURCE_USER: @@ -58,7 +60,6 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="no_devices_found") # Cancel the discovered one. - assert self.hass is not None for flow in in_progress: self.hass.config_entries.flow.async_abort(flow["flow_id"]) @@ -68,8 +69,8 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return self.async_create_entry(title=self._title, data={}) async def async_step_discovery( - self, discovery_info: Dict[str, Any] - ) -> Dict[str, Any]: + self, discovery_info: dict[str, Any] + ) -> dict[str, Any]: """Handle a flow initialized by discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -84,13 +85,12 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_homekit = async_step_discovery async_step_dhcp = async_step_discovery - async def async_step_import(self, _: Optional[Dict[str, Any]]) -> Dict[str, Any]: + async def async_step_import(self, _: dict[str, Any] | None) -> dict[str, Any]: """Handle a flow initialized by import.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") # Cancel other flows. - assert self.hass is not None in_progress = self._async_in_progress() for flow in in_progress: self.hass.config_entries.flow.async_abort(flow["flow_id"]) @@ -134,8 +134,8 @@ class WebhookFlowHandler(config_entries.ConfigFlow): self._allow_multiple = allow_multiple async def async_step_user( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -143,7 +143,6 @@ class WebhookFlowHandler(config_entries.ConfigFlow): if user_input is None: return self.async_show_form(step_id="user") - assert self.hass is not None webhook_id = self.hass.components.webhook.async_generate_id() if ( @@ -182,7 +181,7 @@ def register_webhook_flow( async def webhook_async_remove_entry( - hass: HomeAssistantType, entry: config_entries.ConfigEntry + hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Remove a webhook config entry.""" if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 653d07a333e..795c08dd1c9 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -5,12 +5,14 @@ This module exists of the following parts: - OAuth2 implementation that works with local provided client ID/secret """ +from __future__ import annotations + from abc import ABC, ABCMeta, abstractmethod import asyncio import logging import secrets import time -from typing import Any, Awaitable, Callable, Dict, Optional, cast +from typing import Any, Awaitable, Callable, Dict, cast from aiohttp import client, web import async_timeout @@ -231,10 +233,9 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return {} async def async_step_pick_implementation( - self, user_input: Optional[dict] = None + self, user_input: dict | None = None ) -> dict: """Handle a flow start.""" - assert self.hass implementations = await async_get_implementations(self.hass, self.DOMAIN) if user_input is not None: @@ -244,8 +245,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): if not implementations: return self.async_abort(reason="missing_configuration") - if len(implementations) == 1: - # Pick first implementation as we have only one. + req = http.current_request.get() + if len(implementations) == 1 and req is not None: + # Pick first implementation if we have only one, but only + # if this is triggered by a user interaction (request). self.flow_impl = list(implementations.values())[0] return await self.async_step_auth() @@ -261,8 +264,8 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): ) async def async_step_auth( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Create an entry for auth.""" # Flow has been triggered by external data if user_input: @@ -287,8 +290,8 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step(step_id="auth", url=url) async def async_step_creation( - self, user_input: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: """Create config entry from external data.""" token = await self.flow_impl.async_resolve_external_data(self.external_data) # Force int for non-compliant oauth2 providers @@ -312,24 +315,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) - async def async_step_discovery( - self, discovery_info: Dict[str, Any] - ) -> Dict[str, Any]: - """Handle a flow initialized by discovery.""" - await self.async_set_unique_id(self.DOMAIN) - - assert self.hass is not None - if self.hass.config_entries.async_entries(self.DOMAIN): - return self.async_abort(reason="already_configured") - - return await self.async_step_pick_implementation() - async_step_user = async_step_pick_implementation - async_step_mqtt = async_step_discovery - async_step_ssdp = async_step_discovery - async_step_zeroconf = async_step_discovery - async_step_homekit = async_step_discovery - async_step_dhcp = async_step_discovery @classmethod def async_register_implementation( @@ -356,7 +342,7 @@ def async_register_implementation( async def async_get_implementations( hass: HomeAssistant, domain: str -) -> Dict[str, AbstractOAuth2Implementation]: +) -> dict[str, AbstractOAuth2Implementation]: """Return OAuth2 implementations for specified domain.""" registered = cast( Dict[str, AbstractOAuth2Implementation], @@ -394,7 +380,7 @@ def async_add_implementation_provider( hass: HomeAssistant, provider_domain: str, async_provide_implementation: Callable[ - [HomeAssistant, str], Awaitable[Optional[AbstractOAuth2Implementation]] + [HomeAssistant, str], Awaitable[AbstractOAuth2Implementation | None] ], ) -> None: """Add an implementation provider. @@ -518,7 +504,7 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: @callback -def _decode_jwt(hass: HomeAssistant, encoded: str) -> Optional[dict]: +def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict | None: """JWT encode data.""" secret = cast(str, hass.data.get(DATA_JWT_SECRET)) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 422f940e98e..9b56bb06865 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,4 +1,6 @@ """Helpers for config validation using voluptuous.""" +from __future__ import annotations + from datetime import ( date as date_sys, datetime as datetime_sys, @@ -12,19 +14,7 @@ from numbers import Number import os import re from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed -from typing import ( - Any, - Callable, - Dict, - Hashable, - List, - Optional, - Pattern, - Type, - TypeVar, - Union, - cast, -) +from typing import Any, Callable, Dict, Hashable, Pattern, TypeVar, cast from urllib.parse import urlparse from uuid import UUID @@ -131,7 +121,7 @@ def path(value: Any) -> str: def has_at_least_one_key(*keys: str) -> Callable: """Validate that at least one key exists.""" - def validate(obj: Dict) -> Dict: + def validate(obj: dict) -> dict: """Test keys exist in dict.""" if not isinstance(obj, dict): raise vol.Invalid("expected dictionary") @@ -144,10 +134,10 @@ def has_at_least_one_key(*keys: str) -> Callable: return validate -def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]: +def has_at_most_one_key(*keys: str) -> Callable[[dict], dict]: """Validate that zero keys exist or one key exists.""" - def validate(obj: Dict) -> Dict: + def validate(obj: dict) -> dict: """Test zero keys exist or one key exists in dict.""" if not isinstance(obj, dict): raise vol.Invalid("expected dictionary") @@ -253,7 +243,7 @@ def isdir(value: Any) -> str: return dir_in -def ensure_list(value: Union[T, List[T], None]) -> List[T]: +def ensure_list(value: T | list[T] | None) -> list[T]: """Wrap value in list if it is not one.""" if value is None: return [] @@ -269,7 +259,7 @@ def entity_id(value: Any) -> str: raise vol.Invalid(f"Entity ID {value} is an invalid entity ID") -def entity_ids(value: Union[str, List]) -> List[str]: +def entity_ids(value: str | list) -> list[str]: """Validate Entity IDs.""" if value is None: raise vol.Invalid("Entity IDs can not be None") @@ -284,7 +274,7 @@ comp_entity_ids = vol.Any( ) -def entity_domain(domain: Union[str, List[str]]) -> Callable[[Any], str]: +def entity_domain(domain: str | list[str]) -> Callable[[Any], str]: """Validate that entity belong to domain.""" ent_domain = entities_domain(domain) @@ -298,9 +288,7 @@ def entity_domain(domain: Union[str, List[str]]) -> Callable[[Any], str]: return validate -def entities_domain( - domain: Union[str, List[str]] -) -> Callable[[Union[str, List]], List[str]]: +def entities_domain(domain: str | list[str]) -> Callable[[str | list], list[str]]: """Validate that entities belong to domain.""" if isinstance(domain, str): @@ -312,7 +300,7 @@ def entities_domain( def check_invalid(val: str) -> bool: return val not in domain - def validate(values: Union[str, List]) -> List[str]: + def validate(values: str | list) -> list[str]: """Test if entity domain is domain.""" values = entity_ids(values) for ent_id in values: @@ -325,7 +313,7 @@ def entities_domain( return validate -def enum(enumClass: Type[Enum]) -> vol.All: +def enum(enumClass: type[Enum]) -> vol.All: """Create validator for specified enum.""" return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__) @@ -423,7 +411,7 @@ def time_period_str(value: str) -> timedelta: return offset -def time_period_seconds(value: Union[float, str]) -> timedelta: +def time_period_seconds(value: float | str) -> timedelta: """Validate and transform seconds to a time offset.""" try: return timedelta(seconds=float(value)) @@ -450,7 +438,7 @@ positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) positive_time_period = vol.All(time_period, positive_timedelta) -def remove_falsy(value: List[T]) -> List[T]: +def remove_falsy(value: list[T]) -> list[T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -477,7 +465,7 @@ def slug(value: Any) -> str: def schema_with_slug_keys( - value_schema: Union[T, Callable], *, slug_validator: Callable[[Any], str] = slug + value_schema: T | Callable, *, slug_validator: Callable[[Any], str] = slug ) -> Callable: """Ensure dicts have slugs as keys. @@ -486,7 +474,7 @@ def schema_with_slug_keys( """ schema = vol.Schema({str: value_schema}) - def verify(value: Dict) -> Dict: + def verify(value: dict) -> dict: """Validate all keys are slugs and then the value_schema.""" if not isinstance(value, dict): raise vol.Invalid("expected dictionary") @@ -547,7 +535,7 @@ unit_system = vol.All( ) -def template(value: Optional[Any]) -> template_helper.Template: +def template(value: Any | None) -> template_helper.Template: """Validate a jinja2 template.""" if value is None: raise vol.Invalid("template value is None") @@ -563,7 +551,7 @@ def template(value: Optional[Any]) -> template_helper.Template: raise vol.Invalid(f"invalid template ({ex})") from ex -def dynamic_template(value: Optional[Any]) -> template_helper.Template: +def dynamic_template(value: Any | None) -> template_helper.Template: """Validate a dynamic (non static) jinja2 template.""" if value is None: raise vol.Invalid("template value is None") @@ -632,7 +620,7 @@ def time_zone(value: str) -> str: weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)]) -def socket_timeout(value: Optional[Any]) -> object: +def socket_timeout(value: Any | None) -> object: """Validate timeout float > 0.0. None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object. @@ -681,7 +669,7 @@ def uuid4_hex(value: Any) -> str: return result.hex -def ensure_list_csv(value: Any) -> List: +def ensure_list_csv(value: Any) -> list: """Ensure that input is a list or make one from comma-separated string.""" if isinstance(value, str): return [member.strip() for member in value.split(",")] @@ -709,9 +697,9 @@ class multi_select: def deprecated( key: str, - replacement_key: Optional[str] = None, - default: Optional[Any] = None, -) -> Callable[[Dict], Dict]: + replacement_key: str | None = None, + default: Any | None = None, +) -> Callable[[dict], dict]: """ Log key as deprecated and provide a replacement (if exists). @@ -743,15 +731,24 @@ def deprecated( " please remove it from your configuration" ) - def validator(config: Dict) -> Dict: + def validator(config: dict) -> dict: """Check if key is in config and log warning.""" if key in config: - KeywordStyleAdapter(logging.getLogger(module_name)).warning( - warning, - key=key, - replacement_key=replacement_key, - ) - + try: + KeywordStyleAdapter(logging.getLogger(module_name)).warning( + warning.replace( + "'{key}' option", + f"'{key}' option near {config.__config_file__}:{config.__line__}", # type: ignore + ), + key=key, + replacement_key=replacement_key, + ) + except AttributeError: + KeywordStyleAdapter(logging.getLogger(module_name)).warning( + warning, + key=key, + replacement_key=replacement_key, + ) value = config[key] if replacement_key: config.pop(key) @@ -772,14 +769,14 @@ def deprecated( def key_value_schemas( - key: str, value_schemas: Dict[str, vol.Schema] -) -> Callable[[Any], Dict[str, Any]]: + key: str, value_schemas: dict[str, vol.Schema] +) -> Callable[[Any], dict[str, Any]]: """Create a validator that validates based on a value for specific key. This gives better error messages. """ - def key_value_validator(value: Any) -> Dict[str, Any]: + def key_value_validator(value: Any) -> dict[str, Any]: if not isinstance(value, dict): raise vol.Invalid("Expected a dictionary") @@ -800,10 +797,10 @@ def key_value_schemas( def key_dependency( key: Hashable, dependency: Hashable -) -> Callable[[Dict[Hashable, Any]], Dict[Hashable, Any]]: +) -> Callable[[dict[Hashable, Any]], dict[Hashable, Any]]: """Validate that all dependencies exist for key.""" - def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]: + def validator(value: dict[Hashable, Any]) -> dict[Hashable, Any]: """Test dependencies.""" if not isinstance(value, dict): raise vol.Invalid("key dependencies require a dict") @@ -919,7 +916,7 @@ SERVICE_SCHEMA = vol.All( vol.Optional("data"): vol.All(dict, template_complex), vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, - vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS, + vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template), } ), has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), @@ -1238,7 +1235,7 @@ def determine_script_action(action: dict) -> str: return SCRIPT_ACTION_CALL_SERVICE -ACTION_TYPE_SCHEMAS: Dict[str, Callable[[Any], dict]] = { +ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = { SCRIPT_ACTION_CALL_SERVICE: SERVICE_SCHEMA, SCRIPT_ACTION_DELAY: _SCRIPT_DELAY_SCHEMA, SCRIPT_ACTION_WAIT_TEMPLATE: _SCRIPT_WAIT_TEMPLATE_SCHEMA, diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index e686dd2ae4b..00d12d3ab90 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -1,6 +1,7 @@ """Helpers for the data entry flow.""" +from __future__ import annotations -from typing import Any, Dict +from typing import Any from aiohttp import web import voluptuous as vol @@ -20,7 +21,7 @@ class _BaseFlowManagerView(HomeAssistantView): self._flow_mgr = flow_mgr # pylint: disable=no-self-use - def _prepare_result_json(self, result: Dict[str, Any]) -> Dict[str, Any]: + def _prepare_result_json(self, result: dict[str, Any]) -> dict[str, Any]: """Convert result to JSON.""" if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy() @@ -58,7 +59,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): extra=vol.ALLOW_EXTRA, ) ) - async def post(self, request: web.Request, data: Dict[str, Any]) -> web.Response: + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle a POST request.""" if isinstance(data["handler"], list): handler = tuple(data["handler"]) @@ -99,7 +100,7 @@ class FlowManagerResourceView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema(dict), allow_empty=True) async def post( - self, request: web.Request, flow_id: str, data: Dict[str, Any] + self, request: web.Request, flow_id: str, data: dict[str, Any] ) -> web.Response: """Handle a POST request.""" try: diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 23727c2a00f..705f48bbd70 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -1,7 +1,9 @@ """Debounce helper.""" +from __future__ import annotations + import asyncio from logging import Logger -from typing import Any, Awaitable, Callable, Optional +from typing import Any, Awaitable, Callable from homeassistant.core import HassJob, HomeAssistant, callback @@ -16,7 +18,7 @@ class Debouncer: *, cooldown: float, immediate: bool, - function: Optional[Callable[..., Awaitable[Any]]] = None, + function: Callable[..., Awaitable[Any]] | None = None, ): """Initialize debounce. @@ -29,13 +31,13 @@ class Debouncer: self._function = function self.cooldown = cooldown self.immediate = immediate - self._timer_task: Optional[asyncio.TimerHandle] = None + self._timer_task: asyncio.TimerHandle | None = None self._execute_at_end_of_timer: bool = False self._execute_lock = asyncio.Lock() - self._job: Optional[HassJob] = None if function is None else HassJob(function) + self._job: HassJob | None = None if function is None else HassJob(function) @property - def function(self) -> Optional[Callable[..., Awaitable[Any]]]: + def function(self) -> Callable[..., Awaitable[Any]] | None: """Return the function being wrapped by the Debouncer.""" return self._function diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 7478a7fede9..38b1dfca437 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -1,8 +1,10 @@ """Deprecation helpers for Home Assistant.""" +from __future__ import annotations + import functools import inspect import logging -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable from ..helpers.frame import MissingIntegrationFrame, get_integration_frame @@ -49,8 +51,8 @@ def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: def get_deprecated( - config: Dict[str, Any], new_name: str, old_name: str, default: Optional[Any] = None -) -> Optional[Any]: + config: dict[str, Any], new_name: str, old_name: str, default: Any | None = None +) -> Any | None: """Allow an old config name to be deprecated with a replacement. If the new config isn't found, but the old one is, the old value is used @@ -85,7 +87,7 @@ def deprecated_function(replacement: str) -> Callable[..., Callable]: """Decorate function as deprecated.""" @functools.wraps(func) - def deprecated_func(*args: tuple, **kwargs: Dict[str, Any]) -> Any: + def deprecated_func(*args: tuple, **kwargs: dict[str, Any]) -> Any: """Wrap for the original function.""" logger = logging.getLogger(func.__module__) try: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index d311538f27f..e0e5130a94f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,18 +1,20 @@ """Provide a way to connect entities belonging to one device.""" +from __future__ import annotations + from collections import OrderedDict import logging import time -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, cast import attr from homeassistant.const import EVENT_HOMEASSISTANT_STARTED -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass import homeassistant.util.uuid as uuid_util from .debounce import Debouncer -from .typing import UNDEFINED, HomeAssistantType, UndefinedType +from .typing import UNDEFINED, UndefinedType # mypy: disallow_any_generics @@ -50,21 +52,21 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 class DeviceEntry: """Device Registry Entry.""" - config_entries: Set[str] = attr.ib(converter=set, factory=set) - connections: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set) - identifiers: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set) - manufacturer: Optional[str] = attr.ib(default=None) - model: Optional[str] = attr.ib(default=None) - name: Optional[str] = attr.ib(default=None) - sw_version: Optional[str] = attr.ib(default=None) - via_device_id: Optional[str] = attr.ib(default=None) - area_id: Optional[str] = attr.ib(default=None) - name_by_user: Optional[str] = attr.ib(default=None) - entry_type: Optional[str] = attr.ib(default=None) + config_entries: set[str] = attr.ib(converter=set, factory=set) + connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) + identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) + manufacturer: str | None = attr.ib(default=None) + model: str | None = attr.ib(default=None) + name: str | None = attr.ib(default=None) + sw_version: str | None = attr.ib(default=None) + via_device_id: str | None = attr.ib(default=None) + area_id: str | None = attr.ib(default=None) + name_by_user: str | None = attr.ib(default=None) + entry_type: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) - disabled_by: Optional[str] = attr.ib( + disabled_by: str | None = attr.ib( default=None, validator=attr.validators.in_( ( @@ -75,7 +77,7 @@ class DeviceEntry: ) ), ) - suggested_area: Optional[str] = attr.ib(default=None) + suggested_area: str | None = attr.ib(default=None) @property def disabled(self) -> bool: @@ -87,17 +89,17 @@ class DeviceEntry: class DeletedDeviceEntry: """Deleted Device Registry Entry.""" - config_entries: Set[str] = attr.ib() - connections: Set[Tuple[str, str]] = attr.ib() - identifiers: Set[Tuple[str, str]] = attr.ib() + config_entries: set[str] = attr.ib() + connections: set[tuple[str, str]] = attr.ib() + identifiers: set[tuple[str, str]] = attr.ib() id: str = attr.ib() - orphaned_timestamp: Optional[float] = attr.ib() + orphaned_timestamp: float | None = attr.ib() def to_device_entry( self, config_entry_id: str, - connections: Set[Tuple[str, str]], - identifiers: Set[Tuple[str, str]], + connections: set[tuple[str, str]], + identifiers: set[tuple[str, str]], ) -> DeviceEntry: """Create DeviceEntry from DeletedDeviceEntry.""" return DeviceEntry( @@ -133,27 +135,27 @@ def format_mac(mac: str) -> str: class DeviceRegistry: """Class to hold a registry of devices.""" - devices: Dict[str, DeviceEntry] - deleted_devices: Dict[str, DeletedDeviceEntry] - _devices_index: Dict[str, Dict[str, Dict[Tuple[str, str], str]]] + devices: dict[str, DeviceEntry] + deleted_devices: dict[str, DeletedDeviceEntry] + _devices_index: dict[str, dict[str, dict[tuple[str, str], str]]] - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize the device registry.""" self.hass = hass self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._clear_index() @callback - def async_get(self, device_id: str) -> Optional[DeviceEntry]: + def async_get(self, device_id: str) -> DeviceEntry | None: """Get device.""" return self.devices.get(device_id) @callback def async_get_device( self, - identifiers: Set[Tuple[str, str]], - connections: Optional[Set[Tuple[str, str]]] = None, - ) -> Optional[DeviceEntry]: + identifiers: set[tuple[str, str]], + connections: set[tuple[str, str]] | None = None, + ) -> DeviceEntry | None: """Check if device is registered.""" device_id = self._async_get_device_id_from_index( REGISTERED_DEVICE, identifiers, connections @@ -164,9 +166,9 @@ class DeviceRegistry: def _async_get_deleted_device( self, - identifiers: Set[Tuple[str, str]], - connections: Optional[Set[Tuple[str, str]]], - ) -> Optional[DeletedDeviceEntry]: + identifiers: set[tuple[str, str]], + connections: set[tuple[str, str]] | None, + ) -> DeletedDeviceEntry | None: """Check if device is deleted.""" device_id = self._async_get_device_id_from_index( DELETED_DEVICE, identifiers, connections @@ -178,9 +180,9 @@ class DeviceRegistry: def _async_get_device_id_from_index( self, index: str, - identifiers: Set[Tuple[str, str]], - connections: Optional[Set[Tuple[str, str]]], - ) -> Optional[str]: + identifiers: set[tuple[str, str]], + connections: set[tuple[str, str]] | None, + ) -> str | None: """Check if device has previously been registered.""" devices_index = self._devices_index[index] for identifier in identifiers: @@ -193,7 +195,7 @@ class DeviceRegistry: return devices_index[IDX_CONNECTIONS][connection] return None - def _add_device(self, device: Union[DeviceEntry, DeletedDeviceEntry]) -> None: + def _add_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: """Add a device and index it.""" if isinstance(device, DeletedDeviceEntry): devices_index = self._devices_index[DELETED_DEVICE] @@ -204,7 +206,7 @@ class DeviceRegistry: _add_device_to_index(devices_index, device) - def _remove_device(self, device: Union[DeviceEntry, DeletedDeviceEntry]) -> None: + def _remove_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: """Remove a device and remove it from the index.""" if isinstance(device, DeletedDeviceEntry): devices_index = self._devices_index[DELETED_DEVICE] @@ -243,21 +245,21 @@ class DeviceRegistry: self, *, config_entry_id: str, - connections: Optional[Set[Tuple[str, str]]] = None, - identifiers: Optional[Set[Tuple[str, str]]] = None, - manufacturer: Union[str, None, UndefinedType] = UNDEFINED, - model: Union[str, None, UndefinedType] = UNDEFINED, - name: Union[str, None, UndefinedType] = UNDEFINED, - default_manufacturer: Union[str, None, UndefinedType] = UNDEFINED, - default_model: Union[str, None, UndefinedType] = UNDEFINED, - default_name: Union[str, None, UndefinedType] = UNDEFINED, - sw_version: Union[str, None, UndefinedType] = UNDEFINED, - entry_type: Union[str, None, UndefinedType] = UNDEFINED, - via_device: Optional[Tuple[str, str]] = None, + connections: set[tuple[str, str]] | None = None, + identifiers: set[tuple[str, str]] | None = None, + manufacturer: str | None | UndefinedType = UNDEFINED, + model: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + default_manufacturer: str | None | UndefinedType = UNDEFINED, + default_model: str | None | UndefinedType = UNDEFINED, + default_name: str | None | UndefinedType = UNDEFINED, + sw_version: str | None | UndefinedType = UNDEFINED, + entry_type: str | None | UndefinedType = UNDEFINED, + via_device: tuple[str, str] | None = None, # To disable a device if it gets created - disabled_by: Union[str, None, UndefinedType] = UNDEFINED, - suggested_area: Union[str, None, UndefinedType] = UNDEFINED, - ) -> Optional[DeviceEntry]: + disabled_by: str | None | UndefinedType = UNDEFINED, + suggested_area: str | None | UndefinedType = UNDEFINED, + ) -> DeviceEntry | None: """Get device. Create if it doesn't exist.""" if not identifiers and not connections: return None @@ -294,7 +296,7 @@ class DeviceRegistry: if via_device is not None: via = self.async_get_device({via_device}) - via_device_id: Union[str, UndefinedType] = via.id if via else UNDEFINED + via_device_id: str | UndefinedType = via.id if via else UNDEFINED else: via_device_id = UNDEFINED @@ -318,18 +320,18 @@ class DeviceRegistry: self, device_id: str, *, - area_id: Union[str, None, UndefinedType] = UNDEFINED, - manufacturer: Union[str, None, UndefinedType] = UNDEFINED, - model: Union[str, None, UndefinedType] = UNDEFINED, - name: Union[str, None, UndefinedType] = UNDEFINED, - name_by_user: Union[str, None, UndefinedType] = UNDEFINED, - new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, - sw_version: Union[str, None, UndefinedType] = UNDEFINED, - via_device_id: Union[str, None, UndefinedType] = UNDEFINED, - remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, - disabled_by: Union[str, None, UndefinedType] = UNDEFINED, - suggested_area: Union[str, None, UndefinedType] = UNDEFINED, - ) -> Optional[DeviceEntry]: + area_id: str | None | UndefinedType = UNDEFINED, + manufacturer: str | None | UndefinedType = UNDEFINED, + model: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + name_by_user: str | None | UndefinedType = UNDEFINED, + new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, + sw_version: str | None | UndefinedType = UNDEFINED, + via_device_id: str | None | UndefinedType = UNDEFINED, + remove_config_entry_id: str | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, + suggested_area: str | None | UndefinedType = UNDEFINED, + ) -> DeviceEntry | None: """Update properties of a device.""" return self._async_update_device( device_id, @@ -351,26 +353,26 @@ class DeviceRegistry: self, device_id: str, *, - add_config_entry_id: Union[str, UndefinedType] = UNDEFINED, - remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, - merge_connections: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, - merge_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, - new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, - manufacturer: Union[str, None, UndefinedType] = UNDEFINED, - model: Union[str, None, UndefinedType] = UNDEFINED, - name: Union[str, None, UndefinedType] = UNDEFINED, - sw_version: Union[str, None, UndefinedType] = UNDEFINED, - entry_type: Union[str, None, UndefinedType] = UNDEFINED, - via_device_id: Union[str, None, UndefinedType] = UNDEFINED, - area_id: Union[str, None, UndefinedType] = UNDEFINED, - name_by_user: Union[str, None, UndefinedType] = UNDEFINED, - disabled_by: Union[str, None, UndefinedType] = UNDEFINED, - suggested_area: Union[str, None, UndefinedType] = UNDEFINED, - ) -> Optional[DeviceEntry]: + add_config_entry_id: str | UndefinedType = UNDEFINED, + remove_config_entry_id: str | UndefinedType = UNDEFINED, + merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, + merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, + new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, + manufacturer: str | None | UndefinedType = UNDEFINED, + model: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + sw_version: str | None | UndefinedType = UNDEFINED, + entry_type: str | None | UndefinedType = UNDEFINED, + via_device_id: str | None | UndefinedType = UNDEFINED, + area_id: str | None | UndefinedType = UNDEFINED, + name_by_user: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, + suggested_area: str | None | UndefinedType = UNDEFINED, + ) -> DeviceEntry | None: """Update device attributes.""" old = self.devices[device_id] - changes: Dict[str, Any] = {} + changes: dict[str, Any] = {} config_entries = old.config_entries @@ -529,7 +531,7 @@ class DeviceRegistry: self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: + def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data of device registry to store in a file.""" data = {} @@ -615,12 +617,12 @@ class DeviceRegistry: @callback -def async_get(hass: HomeAssistantType) -> DeviceRegistry: +def async_get(hass: HomeAssistant) -> DeviceRegistry: """Get device registry.""" return cast(DeviceRegistry, hass.data[DATA_REGISTRY]) -async def async_load(hass: HomeAssistantType) -> None: +async def async_load(hass: HomeAssistant) -> None: """Load device registry.""" assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = DeviceRegistry(hass) @@ -628,7 +630,7 @@ async def async_load(hass: HomeAssistantType) -> None: @bind_hass -async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: +async def async_get_registry(hass: HomeAssistant) -> DeviceRegistry: """Get device registry. This is deprecated and will be removed in the future. Use async_get instead. @@ -637,7 +639,7 @@ async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: @callback -def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> List[DeviceEntry]: +def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> list[DeviceEntry]: """Return entries that match an area.""" return [device for device in registry.devices.values() if device.area_id == area_id] @@ -645,7 +647,7 @@ def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> List[Devic @callback def async_entries_for_config_entry( registry: DeviceRegistry, config_entry_id: str -) -> List[DeviceEntry]: +) -> list[DeviceEntry]: """Return entries that match a config entry.""" return [ device @@ -656,7 +658,7 @@ def async_entries_for_config_entry( @callback def async_config_entry_disabled_by_changed( - registry: DeviceRegistry, config_entry: "ConfigEntry" + registry: DeviceRegistry, config_entry: ConfigEntry ) -> None: """Handle a config entry being disabled or enabled. @@ -684,9 +686,9 @@ def async_config_entry_disabled_by_changed( @callback def async_cleanup( - hass: HomeAssistantType, + hass: HomeAssistant, dev_reg: DeviceRegistry, - ent_reg: "entity_registry.EntityRegistry", + ent_reg: entity_registry.EntityRegistry, ) -> None: """Clean up device registry.""" # Find all devices that are referenced by a config_entry. @@ -721,7 +723,7 @@ def async_cleanup( @callback -def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> None: +def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: """Clean up device registry when entities removed.""" from . import entity_registry # pylint: disable=import-outside-toplevel @@ -769,7 +771,7 @@ def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> Non hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) -def _normalize_connections(connections: Set[Tuple[str, str]]) -> Set[Tuple[str, str]]: +def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, str]]: """Normalize connections to ensure we can match mac addresses.""" return { (key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value) @@ -778,8 +780,8 @@ def _normalize_connections(connections: Set[Tuple[str, str]]) -> Set[Tuple[str, def _add_device_to_index( - devices_index: Dict[str, Dict[Tuple[str, str], str]], - device: Union[DeviceEntry, DeletedDeviceEntry], + devices_index: dict[str, dict[tuple[str, str], str]], + device: DeviceEntry | DeletedDeviceEntry, ) -> None: """Add a device to the index.""" for identifier in device.identifiers: @@ -789,8 +791,8 @@ def _add_device_to_index( def _remove_device_from_index( - devices_index: Dict[str, Dict[Tuple[str, str], str]], - device: Union[DeviceEntry, DeletedDeviceEntry], + devices_index: dict[str, dict[tuple[str, str], str]], + device: DeviceEntry | DeletedDeviceEntry, ) -> None: """Remove a device from the index.""" for identifier in device.identifiers: diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 0770e6798f1..53dbca867d7 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,62 +5,57 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -from typing import Any, Callable, Collection, Dict, Optional, Union +from __future__ import annotations + +from typing import Any, Callable, TypedDict from homeassistant import core, setup -from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED from homeassistant.core import CALLBACK_TYPE -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import bind_hass -from homeassistant.util.async_ import run_callback_threadsafe +from .dispatcher import async_dispatcher_connect, async_dispatcher_send +from .typing import ConfigType, DiscoveryInfoType + +SIGNAL_PLATFORM_DISCOVERED = "discovery.platform_discovered_{}" EVENT_LOAD_PLATFORM = "load_platform.{}" ATTR_PLATFORM = "platform" +ATTR_DISCOVERED = "discovered" # mypy: disallow-any-generics -@bind_hass -def listen( - hass: core.HomeAssistant, - service: Union[str, Collection[str]], - callback: CALLBACK_TYPE, -) -> None: - """Set up listener for discovery of specific service. +class DiscoveryDict(TypedDict): + """Discovery data.""" - Service can be a string or a list/tuple. - """ - run_callback_threadsafe(hass.loop, async_listen, hass, service, callback).result() + service: str + platform: str | None + discovered: DiscoveryInfoType | None @core.callback @bind_hass def async_listen( hass: core.HomeAssistant, - service: Union[str, Collection[str]], + service: str, callback: CALLBACK_TYPE, ) -> None: """Set up listener for discovery of specific service. Service can be a string or a list/tuple. """ - if isinstance(service, str): - service = (service,) - else: - service = tuple(service) - job = core.HassJob(callback) - async def discovery_event_listener(event: core.Event) -> None: + async def discovery_event_listener(discovered: DiscoveryDict) -> None: """Listen for discovery events.""" - if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service: - task = hass.async_run_hass_job( - job, event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED) - ) - if task: - await task + task = hass.async_run_hass_job( + job, discovered["service"], discovered["discovered"] + ) + if task: + await task - hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener) + async_dispatcher_connect( + hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_event_listener + ) @bind_hass @@ -83,37 +78,28 @@ def discover( async def async_discover( hass: core.HomeAssistant, service: str, - discovered: Optional[DiscoveryInfoType], - component: Optional[str], + discovered: DiscoveryInfoType | None, + component: str | None, hass_config: ConfigType, ) -> None: """Fire discovery event. Can ensure a component is loaded.""" if component is not None and component not in hass.config.components: await setup.async_setup_component(hass, component, hass_config) - data: Dict[str, Any] = {ATTR_SERVICE: service} + data: DiscoveryDict = { + "service": service, + "platform": None, + "discovered": discovered, + } - if discovered is not None: - data[ATTR_DISCOVERED] = discovered - - hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data) - - -@bind_hass -def listen_platform( - hass: core.HomeAssistant, component: str, callback: CALLBACK_TYPE -) -> None: - """Register a platform loader listener.""" - run_callback_threadsafe( - hass.loop, async_listen_platform, hass, component, callback - ).result() + async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data) @bind_hass def async_listen_platform( hass: core.HomeAssistant, component: str, - callback: Callable[[str, Optional[Dict[str, Any]]], Any], + callback: Callable[[str, dict[str, Any] | None], Any], ) -> None: """Register a platform loader listener. @@ -122,21 +108,20 @@ def async_listen_platform( service = EVENT_LOAD_PLATFORM.format(component) job = core.HassJob(callback) - async def discovery_platform_listener(event: core.Event) -> None: + async def discovery_platform_listener(discovered: DiscoveryDict) -> None: """Listen for platform discovery events.""" - if event.data.get(ATTR_SERVICE) != service: - return - - platform = event.data.get(ATTR_PLATFORM) + platform = discovered["platform"] if not platform: return - task = hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED)) + task = hass.async_run_hass_job(job, platform, discovered.get("discovered")) if task: await task - hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener) + async_dispatcher_connect( + hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_platform_listener + ) @bind_hass @@ -147,16 +132,7 @@ def load_platform( discovered: DiscoveryInfoType, hass_config: ConfigType, ) -> None: - """Load a component and platform dynamically. - - Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be - fired to load the platform. The event will contain: - { ATTR_SERVICE = EVENT_LOAD_PLATFORM + '.' + <> - ATTR_PLATFORM = <> - ATTR_DISCOVERED = <> } - - Use `listen_platform` to register a callback for these events. - """ + """Load a component and platform dynamically.""" hass.add_job( async_load_platform( # type: ignore hass, component, platform, discovered, hass_config @@ -174,18 +150,10 @@ async def async_load_platform( ) -> None: """Load a component and platform dynamically. - Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be - fired to load the platform. The event will contain: - { ATTR_SERVICE = EVENT_LOAD_PLATFORM + '.' + <> - ATTR_PLATFORM = <> - ATTR_DISCOVERED = <> } - - Use `listen_platform` to register a callback for these events. + Use `async_listen_platform` to register a callback for these events. Warning: Do not await this inside a setup method to avoid a dead lock. Use `hass.async_create_task(async_load_platform(..))` instead. - - This method is a coroutine. """ assert hass_config, "You need to pass in the real hass config" @@ -194,16 +162,16 @@ async def async_load_platform( if component not in hass.config.components: setup_success = await setup.async_setup_component(hass, component, hass_config) - # No need to fire event if we could not set up component + # No need to send signal if we could not set up component if not setup_success: return - data: Dict[str, Any] = { - ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component), - ATTR_PLATFORM: platform, + service = EVENT_LOAD_PLATFORM.format(component) + + data: DiscoveryDict = { + "service": service, + "platform": platform, + "discovered": discovered, } - if discovered is not None: - data[ATTR_DISCOVERED] = discovered - - hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, data) + async_dispatcher_send(hass, SIGNAL_PLATFORM_DISCOVERED.format(service), data) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index cdf24ec23e9..2b365412e27 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -2,20 +2,18 @@ import logging from typing import Any, Callable -from homeassistant.core import HassJob, callback +from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception -from .typing import HomeAssistantType - _LOGGER = logging.getLogger(__name__) DATA_DISPATCHER = "dispatcher" @bind_hass def dispatcher_connect( - hass: HomeAssistantType, signal: str, target: Callable[..., None] + hass: HomeAssistant, signal: str, target: Callable[..., None] ) -> Callable[[], None]: """Connect a callable function to a signal.""" async_unsub = run_callback_threadsafe( @@ -32,7 +30,7 @@ def dispatcher_connect( @callback @bind_hass def async_dispatcher_connect( - hass: HomeAssistantType, signal: str, target: Callable[..., Any] + hass: HomeAssistant, signal: str, target: Callable[..., Any] ) -> Callable[[], None]: """Connect a callable function to a signal. @@ -69,14 +67,14 @@ def async_dispatcher_connect( @bind_hass -def dispatcher_send(hass: HomeAssistantType, signal: str, *args: Any) -> None: +def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: """Send signal and data.""" hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) @callback @bind_hass -def async_dispatcher_send(hass: HomeAssistantType, signal: str, *args: Any) -> None: +def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: """Send signal and data. This method must be run in the event loop. diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7d0e38ab119..0074c0ba5e8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,11 +1,14 @@ """An abstract class for entities.""" +from __future__ import annotations + from abc import ABC import asyncio +from collections.abc import Mapping from datetime import datetime, timedelta import functools as ft import logging from timeit import default_timer as timer -from typing import Any, Awaitable, Dict, Iterable, List, Optional +from typing import Any, Awaitable, Iterable from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( @@ -42,16 +45,16 @@ SOURCE_PLATFORM_CONFIG = "platform_config" @callback @bind_hass -def entity_sources(hass: HomeAssistant) -> Dict[str, Dict[str, str]]: +def entity_sources(hass: HomeAssistant) -> dict[str, dict[str, str]]: """Get the entity sources.""" return hass.data.get(DATA_ENTITY_SOURCE, {}) def generate_entity_id( entity_id_format: str, - name: Optional[str], - current_ids: Optional[List[str]] = None, - hass: Optional[HomeAssistant] = None, + name: str | None, + current_ids: list[str] | None = None, + hass: HomeAssistant | None = None, ) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" return async_generate_entity_id(entity_id_format, name, current_ids, hass) @@ -60,9 +63,9 @@ def generate_entity_id( @callback def async_generate_entity_id( entity_id_format: str, - name: Optional[str], - current_ids: Optional[Iterable[str]] = None, - hass: Optional[HomeAssistant] = None, + name: str | None, + current_ids: Iterable[str] | None = None, + hass: HomeAssistant | None = None, ) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" name = (name or DEVICE_DEFAULT_NAME).lower() @@ -89,13 +92,16 @@ class Entity(ABC): # SAFE TO OVERWRITE # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. - entity_id = None # type: str + entity_id: str = None # type: ignore # Owning hass instance. Will be set by EntityPlatform - hass: Optional[HomeAssistant] = None + # While not purely typed, it makes typehinting more useful for us + # and removes the need for constant None checks or asserts. + # Ignore types: https://github.com/PyCQA/pylint/issues/3167 + hass: HomeAssistant = None # type: ignore # Owning platform instance. Will be set by EntityPlatform - platform: Optional[EntityPlatform] = None + platform: EntityPlatform | None = None # If we reported if this entity was slow _slow_reported = False @@ -107,17 +113,17 @@ class Entity(ABC): _update_staged = False # Process updates in parallel - parallel_updates: Optional[asyncio.Semaphore] = None + parallel_updates: asyncio.Semaphore | None = None # Entry in the entity registry - registry_entry: Optional[RegistryEntry] = None + registry_entry: RegistryEntry | None = None # Hold list for functions to call on remove. - _on_remove: Optional[List[CALLBACK_TYPE]] = None + _on_remove: list[CALLBACK_TYPE] | None = None # Context - _context: Optional[Context] = None - _context_set: Optional[datetime] = None + _context: Context | None = None + _context_set: datetime | None = None # If entity is added to an entity platform _added = False @@ -131,12 +137,12 @@ class Entity(ABC): return True @property - def unique_id(self) -> Optional[str]: + def unique_id(self) -> str | None: """Return a unique ID.""" return None @property - def name(self) -> Optional[str]: + def name(self) -> str | None: """Return the name of the entity.""" return None @@ -146,7 +152,7 @@ class Entity(ABC): return STATE_UNKNOWN @property - def capability_attributes(self) -> Optional[Dict[str, Any]]: + def capability_attributes(self) -> Mapping[str, Any] | None: """Return the capability attributes. Attributes that explain the capabilities of an entity. @@ -157,17 +163,26 @@ class Entity(ABC): return None @property - def state_attributes(self) -> Optional[Dict[str, Any]]: + def state_attributes(self) -> dict[str, Any] | None: """Return the state attributes. - Implemented by component base class. Convention for attribute names - is lowercase snake_case. + Implemented by component base class, should not be extended by integrations. + Convention for attribute names is lowercase snake_case. """ return None @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: - """Return device specific state attributes. + def device_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes. + + This method is deprecated, platform classes should implement + extra_state_attributes instead. + """ + return None + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes. Implemented by platform classes. Convention for attribute names is lowercase snake_case. @@ -175,7 +190,7 @@ class Entity(ABC): return None @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> Mapping[str, Any] | None: """Return device specific attributes. Implemented by platform classes. @@ -183,22 +198,22 @@ class Entity(ABC): return None @property - def device_class(self) -> Optional[str]: + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" return None @property - def unit_of_measurement(self) -> Optional[str]: + def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" return None @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" return None @property - def entity_picture(self) -> Optional[str]: + def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" return None @@ -222,7 +237,7 @@ class Entity(ABC): return False @property - def supported_features(self) -> Optional[int]: + def supported_features(self) -> int | None: """Flag supported features.""" return None @@ -319,7 +334,12 @@ class Entity(ABC): sstate = self.state state = STATE_UNKNOWN if sstate is None else str(sstate) attr.update(self.state_attributes or {}) - attr.update(self.device_state_attributes or {}) + extra_state_attributes = self.extra_state_attributes + # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + if extra_state_attributes is None: + extra_state_attributes = self.device_state_attributes + attr.update(extra_state_attributes or {}) unit_of_measurement = self.unit_of_measurement if unit_of_measurement is not None: @@ -377,7 +397,6 @@ class Entity(ABC): ) # Overwrite properties that have been set in the config file. - assert self.hass is not None if DATA_CUSTOMIZE in self.hass.data: attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)) @@ -418,7 +437,6 @@ class Entity(ABC): If state is changed more than once before the ha state change task has been executed, the intermediate state transitions will be missed. """ - assert self.hass is not None self.hass.add_job(self.async_update_ha_state(force_refresh)) # type: ignore @callback @@ -434,7 +452,6 @@ class Entity(ABC): been executed, the intermediate state transitions will be missed. """ if force_refresh: - assert self.hass is not None self.hass.async_create_task(self.async_update_ha_state(force_refresh)) else: self.async_write_ha_state() @@ -502,7 +519,7 @@ class Entity(ABC): self, hass: HomeAssistant, platform: EntityPlatform, - parallel_updates: Optional[asyncio.Semaphore], + parallel_updates: asyncio.Semaphore | None, ) -> None: """Start adding an entity to a platform.""" if self._added: @@ -518,7 +535,7 @@ class Entity(ABC): @callback def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" - self.hass = None + self.hass = None # type: ignore self.platform = None self.parallel_updates = None self._added = False @@ -539,8 +556,6 @@ class Entity(ABC): If the entity doesn't have a non disabled entry in the entity registry, or if force_remove=True, its state will be removed. """ - assert self.hass is not None - if self.platform and not self._added: raise HomeAssistantError( f"Entity {self.entity_id} async_remove called twice" @@ -583,8 +598,6 @@ class Entity(ABC): Not to be extended by integrations. """ - assert self.hass is not None - if self.platform: info = {"domain": self.platform.platform_name} @@ -614,7 +627,6 @@ class Entity(ABC): Not to be extended by integrations. """ if self.platform: - assert self.hass is not None self.hass.data[DATA_ENTITY_SOURCE].pop(self.entity_id) async def _async_registry_updated(self, event: Event) -> None: @@ -628,7 +640,6 @@ class Entity(ABC): if data["action"] != "update": return - assert self.hass is not None ent_reg = await self.hass.helpers.entity_registry.async_get_registry() old = self.registry_entry self.registry_entry = ent_reg.async_get(data["entity_id"]) @@ -703,7 +714,6 @@ class ToggleEntity(Entity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - assert self.hass is not None await self.hass.async_add_executor_job(ft.partial(self.turn_on, **kwargs)) def turn_off(self, **kwargs: Any) -> None: @@ -712,7 +722,6 @@ class ToggleEntity(Entity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - assert self.hass is not None await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs)) def toggle(self, **kwargs: Any) -> None: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 6fb8696d845..17131665240 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -1,10 +1,12 @@ """Helpers for components that manage entities.""" +from __future__ import annotations + import asyncio from datetime import timedelta from itertools import chain import logging from types import ModuleType -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union +from typing import Any, Callable, Iterable import voluptuous as vol @@ -76,10 +78,10 @@ class EntityComponent: self.domain = domain self.scan_interval = scan_interval - self.config: Optional[ConfigType] = None + self.config: ConfigType | None = None - self._platforms: Dict[ - Union[str, Tuple[str, Optional[timedelta], Optional[str]]], EntityPlatform + self._platforms: dict[ + str | tuple[str, timedelta | None, str | None], EntityPlatform ] = {domain: self._async_init_entity_platform(domain, None)} self.async_add_entities = self._platforms[domain].async_add_entities self.add_entities = self._platforms[domain].add_entities @@ -93,7 +95,7 @@ class EntityComponent: platform.entities.values() for platform in self._platforms.values() ) - def get_entity(self, entity_id: str) -> Optional[entity.Entity]: + def get_entity(self, entity_id: str) -> entity.Entity | None: """Get an entity.""" for platform in self._platforms.values(): entity_obj = platform.entities.get(entity_id) @@ -125,7 +127,7 @@ class EntityComponent: # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.helpers.discovery.async_load_platform() async def component_platform_discovered( - platform: str, info: Optional[Dict[str, Any]] + platform: str, info: dict[str, Any] | None ) -> None: """Handle the loading of a platform.""" await self.async_setup_platform(platform, {}, info) @@ -176,7 +178,7 @@ class EntityComponent: async def async_extract_from_service( self, service_call: ServiceCall, expand_group: bool = True - ) -> List[entity.Entity]: + ) -> list[entity.Entity]: """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. @@ -191,9 +193,9 @@ class EntityComponent: def async_register_entity_service( self, name: str, - schema: Union[Dict[str, Any], vol.Schema], - func: Union[str, Callable[..., Any]], - required_features: Optional[List[int]] = None, + schema: dict[str, Any] | vol.Schema, + func: str | Callable[..., Any], + required_features: list[int] | None = None, ) -> None: """Register an entity service.""" if isinstance(schema, dict): @@ -211,7 +213,7 @@ class EntityComponent: self, platform_type: str, platform_config: ConfigType, - discovery_info: Optional[DiscoveryInfoType] = None, + discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a platform for this component.""" if self.config is None: @@ -274,7 +276,7 @@ class EntityComponent: async def async_prepare_reload( self, *, skip_reset: bool = False - ) -> Optional[ConfigType]: + ) -> ConfigType | None: """Prepare reloading this entity component. This method must be run in the event loop. @@ -303,9 +305,9 @@ class EntityComponent: def _async_init_entity_platform( self, platform_type: str, - platform: Optional[ModuleType], - scan_interval: Optional[timedelta] = None, - entity_namespace: Optional[str] = None, + platform: ModuleType | None, + scan_interval: timedelta | None = None, + entity_namespace: str | None = None, ) -> EntityPlatform: """Initialize an entity platform.""" if scan_interval is None: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2caf7fe46ab..b9d603ba5e1 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -6,20 +6,25 @@ from contextvars import ContextVar from datetime import datetime, timedelta from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Callable, Coroutine, Iterable from homeassistant import config_entries from homeassistant.const import ATTR_RESTORED, DEVICE_DEFAULT_NAME from homeassistant.core import ( CALLBACK_TYPE, + HomeAssistant, ServiceCall, callback, split_entity_id, valid_entity_id, ) from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers import config_validation as cv, service -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dev_reg, + entity_registry as ent_reg, + service, +) from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION @@ -45,13 +50,13 @@ class EntityPlatform: def __init__( self, *, - hass: HomeAssistantType, + hass: HomeAssistant, logger: Logger, domain: str, platform_name: str, - platform: Optional[ModuleType], + platform: ModuleType | None, scan_interval: timedelta, - entity_namespace: Optional[str], + entity_namespace: str | None, ): """Initialize the entity platform.""" self.hass = hass @@ -61,18 +66,18 @@ class EntityPlatform: self.platform = platform self.scan_interval = scan_interval self.entity_namespace = entity_namespace - self.config_entry: Optional[config_entries.ConfigEntry] = None - self.entities: Dict[str, Entity] = {} # pylint: disable=used-before-assignment - self._tasks: List[asyncio.Future] = [] + self.config_entry: config_entries.ConfigEntry | None = None + self.entities: dict[str, Entity] = {} + self._tasks: list[asyncio.Future] = [] # Stop tracking tasks after setup is completed self._setup_complete = False # Method to cancel the state change listener - self._async_unsub_polling: Optional[CALLBACK_TYPE] = None + self._async_unsub_polling: CALLBACK_TYPE | None = None # Method to cancel the retry of setup - self._async_cancel_retry_setup: Optional[CALLBACK_TYPE] = None - self._process_updates: Optional[asyncio.Lock] = None + self._async_cancel_retry_setup: CALLBACK_TYPE | None = None + self._process_updates: asyncio.Lock | None = None - self.parallel_updates: Optional[asyncio.Semaphore] = None + self.parallel_updates: asyncio.Semaphore | None = None # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities @@ -89,7 +94,7 @@ class EntityPlatform: @callback def _get_parallel_updates_semaphore( self, entity_has_async_update: bool - ) -> Optional[asyncio.Semaphore]: + ) -> asyncio.Semaphore | None: """Get or create a semaphore for parallel updates. Semaphore will be created on demand because we base it off if update method is async or not. @@ -298,8 +303,8 @@ class EntityPlatform: hass = self.hass - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = dev_reg.async_get(hass) + entity_registry = ent_reg.async_get(hass) tasks = [ self._async_add_entity( # type: ignore entity, update_before_add, entity_registry, device_registry @@ -364,7 +369,7 @@ class EntityPlatform: return requested_entity_id = None - suggested_object_id: Optional[str] = None + suggested_object_id: str | None = None # Get entity_id from unique ID registration if entity.unique_id is not None: @@ -378,7 +383,7 @@ class EntityPlatform: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" if self.config_entry is not None: - config_entry_id: Optional[str] = self.config_entry.entry_id + config_entry_id: str | None = self.config_entry.entry_id else: config_entry_id = None @@ -408,7 +413,7 @@ class EntityPlatform: if device: device_id = device.id - disabled_by: Optional[str] = None + disabled_by: str | None = None if not entity.entity_registry_enabled_default: disabled_by = DISABLED_INTEGRATION @@ -550,7 +555,7 @@ class EntityPlatform: async def async_extract_from_service( self, service_call: ServiceCall, expand_group: bool = True - ) -> List[Entity]: + ) -> list[Entity]: """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. @@ -621,15 +626,15 @@ class EntityPlatform: await asyncio.gather(*tasks) -current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( +current_platform: ContextVar[EntityPlatform | None] = ContextVar( "current_platform", default=None ) @callback def async_get_platforms( - hass: HomeAssistantType, integration_name: str -) -> List[EntityPlatform]: + hass: HomeAssistant, integration_name: str +) -> list[EntityPlatform]: """Find existing platforms.""" if ( DATA_ENTITY_PLATFORM not in hass.data @@ -637,6 +642,6 @@ def async_get_platforms( ): return [] - platforms: List[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name] + platforms: list[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name] return platforms diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 8a7a4de970a..db16b3cc0b1 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,20 +7,11 @@ The Entity Registry will persist itself 10 seconds after a new entity is registered. Registering a new entity while a timer is in progress resets the timer. """ +from __future__ import annotations + from collections import OrderedDict import logging -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Tuple, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Callable, Iterable, cast import attr @@ -34,17 +25,23 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, ) -from homeassistant.core import Event, callback, split_entity_id, valid_entity_id +from homeassistant.core import ( + Event, + HomeAssistant, + callback, + split_entity_id, + valid_entity_id, +) from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml -from .typing import UNDEFINED, HomeAssistantType, UndefinedType +from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntry # noqa: F401 + from homeassistant.config_entries import ConfigEntry PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" @@ -80,12 +77,12 @@ class RegistryEntry: entity_id: str = attr.ib() unique_id: str = attr.ib() platform: str = attr.ib() - name: Optional[str] = attr.ib(default=None) - icon: Optional[str] = attr.ib(default=None) - device_id: Optional[str] = attr.ib(default=None) - area_id: Optional[str] = attr.ib(default=None) - config_entry_id: Optional[str] = attr.ib(default=None) - disabled_by: Optional[str] = attr.ib( + name: str | None = attr.ib(default=None) + icon: str | None = attr.ib(default=None) + device_id: str | None = attr.ib(default=None) + area_id: str | None = attr.ib(default=None) + config_entry_id: str | None = attr.ib(default=None) + disabled_by: str | None = attr.ib( default=None, validator=attr.validators.in_( ( @@ -98,13 +95,13 @@ class RegistryEntry: ) ), ) - capabilities: Optional[Dict[str, Any]] = attr.ib(default=None) + capabilities: dict[str, Any] | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) - device_class: Optional[str] = attr.ib(default=None) - unit_of_measurement: Optional[str] = attr.ib(default=None) + device_class: str | None = attr.ib(default=None) + unit_of_measurement: str | None = attr.ib(default=None) # As set by integration - original_name: Optional[str] = attr.ib(default=None) - original_icon: Optional[str] = attr.ib(default=None) + original_name: str | None = attr.ib(default=None) + original_icon: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) @domain.default @@ -118,9 +115,9 @@ class RegistryEntry: return self.disabled_by is not None @callback - def write_unavailable_state(self, hass: HomeAssistantType) -> None: + def write_unavailable_state(self, hass: HomeAssistant) -> None: """Write the unavailable state to the state machine.""" - attrs: Dict[str, Any] = {ATTR_RESTORED: True} + attrs: dict[str, Any] = {ATTR_RESTORED: True} if self.capabilities is not None: attrs.update(self.capabilities) @@ -148,11 +145,11 @@ class RegistryEntry: class EntityRegistry: """Class to hold a registry of entities.""" - def __init__(self, hass: HomeAssistantType): + def __init__(self, hass: HomeAssistant): """Initialize the registry.""" self.hass = hass - self.entities: Dict[str, RegistryEntry] - self._index: Dict[Tuple[str, str, str], str] = {} + self.entities: dict[str, RegistryEntry] + self._index: dict[tuple[str, str, str], str] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified @@ -161,7 +158,7 @@ class EntityRegistry: @callback def async_get_device_class_lookup(self, domain_device_classes: set) -> dict: """Return a lookup for the device class by domain.""" - lookup: Dict[str, Dict[Tuple[Any, Any], str]] = {} + lookup: dict[str, dict[tuple[Any, Any], str]] = {} for entity in self.entities.values(): if not entity.device_id: continue @@ -180,14 +177,14 @@ class EntityRegistry: return entity_id in self.entities @callback - def async_get(self, entity_id: str) -> Optional[RegistryEntry]: + def async_get(self, entity_id: str) -> RegistryEntry | None: """Get EntityEntry for an entity_id.""" return self.entities.get(entity_id) @callback def async_get_entity_id( self, domain: str, platform: str, unique_id: str - ) -> Optional[str]: + ) -> str | None: """Check if an entity_id is currently registered.""" return self._index.get((domain, platform, unique_id)) @@ -196,7 +193,7 @@ class EntityRegistry: self, domain: str, suggested_object_id: str, - known_object_ids: Optional[Iterable[str]] = None, + known_object_ids: Iterable[str] | None = None, ) -> str: """Generate an entity ID that does not conflict. @@ -226,20 +223,20 @@ class EntityRegistry: unique_id: str, *, # To influence entity ID generation - suggested_object_id: Optional[str] = None, - known_object_ids: Optional[Iterable[str]] = None, + suggested_object_id: str | None = None, + known_object_ids: Iterable[str] | None = None, # To disable an entity if it gets created - disabled_by: Optional[str] = None, + disabled_by: str | None = None, # Data that we want entry to have - config_entry: Optional["ConfigEntry"] = None, - device_id: Optional[str] = None, - area_id: Optional[str] = None, - capabilities: Optional[Dict[str, Any]] = None, - supported_features: Optional[int] = None, - device_class: Optional[str] = None, - unit_of_measurement: Optional[str] = None, - original_name: Optional[str] = None, - original_icon: Optional[str] = None, + config_entry: ConfigEntry | None = None, + device_id: str | None = None, + area_id: str | None = None, + capabilities: dict[str, Any] | None = None, + supported_features: int | None = None, + device_class: str | None = None, + unit_of_measurement: str | None = None, + original_name: str | None = None, + original_icon: str | None = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -363,12 +360,12 @@ class EntityRegistry: self, entity_id: str, *, - name: Union[str, None, UndefinedType] = UNDEFINED, - icon: Union[str, None, UndefinedType] = UNDEFINED, - area_id: Union[str, None, UndefinedType] = UNDEFINED, - new_entity_id: Union[str, UndefinedType] = UNDEFINED, - new_unique_id: Union[str, UndefinedType] = UNDEFINED, - disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + area_id: str | None | UndefinedType = UNDEFINED, + new_entity_id: str | UndefinedType = UNDEFINED, + new_unique_id: str | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Update properties of an entity.""" return self._async_update_entity( @@ -386,20 +383,20 @@ class EntityRegistry: self, entity_id: str, *, - name: Union[str, None, UndefinedType] = UNDEFINED, - icon: Union[str, None, UndefinedType] = UNDEFINED, - config_entry_id: Union[str, None, UndefinedType] = UNDEFINED, - new_entity_id: Union[str, UndefinedType] = UNDEFINED, - device_id: Union[str, None, UndefinedType] = UNDEFINED, - area_id: Union[str, None, UndefinedType] = UNDEFINED, - new_unique_id: Union[str, UndefinedType] = UNDEFINED, - disabled_by: Union[str, None, UndefinedType] = UNDEFINED, - capabilities: Union[Dict[str, Any], None, UndefinedType] = UNDEFINED, - supported_features: Union[int, UndefinedType] = UNDEFINED, - device_class: Union[str, None, UndefinedType] = UNDEFINED, - unit_of_measurement: Union[str, None, UndefinedType] = UNDEFINED, - original_name: Union[str, None, UndefinedType] = UNDEFINED, - original_icon: Union[str, None, UndefinedType] = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + icon: str | None | UndefinedType = UNDEFINED, + config_entry_id: str | None | UndefinedType = UNDEFINED, + new_entity_id: str | UndefinedType = UNDEFINED, + device_id: str | None | UndefinedType = UNDEFINED, + area_id: str | None | UndefinedType = UNDEFINED, + new_unique_id: str | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, + capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED, + supported_features: int | UndefinedType = UNDEFINED, + device_class: str | None | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, + original_name: str | None | UndefinedType = UNDEFINED, + original_icon: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] @@ -479,7 +476,7 @@ class EntityRegistry: old_conf_load_func=load_yaml, old_conf_migrate_func=_async_migrate, ) - entities: Dict[str, RegistryEntry] = OrderedDict() + entities: dict[str, RegistryEntry] = OrderedDict() if data is not None: for entity in data["entities"]: @@ -516,7 +513,7 @@ class EntityRegistry: self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self) -> Dict[str, Any]: + def _data_to_save(self) -> dict[str, Any]: """Return data of entity registry to store in a file.""" data = {} @@ -581,12 +578,12 @@ class EntityRegistry: @callback -def async_get(hass: HomeAssistantType) -> EntityRegistry: +def async_get(hass: HomeAssistant) -> EntityRegistry: """Get entity registry.""" return cast(EntityRegistry, hass.data[DATA_REGISTRY]) -async def async_load(hass: HomeAssistantType) -> None: +async def async_load(hass: HomeAssistant) -> None: """Load entity registry.""" assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = EntityRegistry(hass) @@ -594,7 +591,7 @@ async def async_load(hass: HomeAssistantType) -> None: @bind_hass -async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: +async def async_get_registry(hass: HomeAssistant) -> EntityRegistry: """Get entity registry. This is deprecated and will be removed in the future. Use async_get instead. @@ -605,7 +602,7 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: @callback def async_entries_for_device( registry: EntityRegistry, device_id: str, include_disabled_entities: bool = False -) -> List[RegistryEntry]: +) -> list[RegistryEntry]: """Return entries that match a device.""" return [ entry @@ -618,7 +615,7 @@ def async_entries_for_device( @callback def async_entries_for_area( registry: EntityRegistry, area_id: str -) -> List[RegistryEntry]: +) -> list[RegistryEntry]: """Return entries that match an area.""" return [entry for entry in registry.entities.values() if entry.area_id == area_id] @@ -626,7 +623,7 @@ def async_entries_for_area( @callback def async_entries_for_config_entry( registry: EntityRegistry, config_entry_id: str -) -> List[RegistryEntry]: +) -> list[RegistryEntry]: """Return entries that match a config entry.""" return [ entry @@ -637,7 +634,7 @@ def async_entries_for_config_entry( @callback def async_config_entry_disabled_by_changed( - registry: EntityRegistry, config_entry: "ConfigEntry" + registry: EntityRegistry, config_entry: ConfigEntry ) -> None: """Handle a config entry being disabled or enabled. @@ -665,7 +662,7 @@ def async_config_entry_disabled_by_changed( ) -async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]: +async def _async_migrate(entities: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: """Migrate the YAML config file to storage helper format.""" return { "entities": [ @@ -675,9 +672,7 @@ async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, A @callback -def async_setup_entity_restore( - hass: HomeAssistantType, registry: EntityRegistry -) -> None: +def async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -> None: """Set up the entity restore mechanism.""" @callback @@ -719,9 +714,9 @@ def async_setup_entity_restore( async def async_migrate_entries( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry_id: str, - entry_callback: Callable[[RegistryEntry], Optional[dict]], + entry_callback: Callable[[RegistryEntry], dict | None], ) -> None: """Migrator of unique IDs.""" ent_reg = await async_get_registry(hass) diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index 7f44e8b2768..57dbb34c560 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -1,8 +1,10 @@ """A class to hold entity values.""" +from __future__ import annotations + from collections import OrderedDict import fnmatch import re -from typing import Any, Dict, Optional, Pattern +from typing import Any, Pattern from homeassistant.core import split_entity_id @@ -14,17 +16,17 @@ class EntityValues: def __init__( self, - exact: Optional[Dict[str, Dict[str, str]]] = None, - domain: Optional[Dict[str, Dict[str, str]]] = None, - glob: Optional[Dict[str, Dict[str, str]]] = None, + exact: dict[str, dict[str, str]] | None = None, + domain: dict[str, dict[str, str]] | None = None, + glob: dict[str, dict[str, str]] | None = None, ) -> None: """Initialize an EntityConfigDict.""" - self._cache: Dict[str, Dict[str, str]] = {} + self._cache: dict[str, dict[str, str]] = {} self._exact = exact self._domain = domain if glob is None: - compiled: Optional[Dict[Pattern[str], Any]] = None + compiled: dict[Pattern[str], Any] | None = None else: compiled = OrderedDict() for key, value in glob.items(): @@ -32,7 +34,7 @@ class EntityValues: self._glob = compiled - def get(self, entity_id: str) -> Dict[str, str]: + def get(self, entity_id: str) -> dict[str, str]: """Get config for an entity id.""" if entity_id in self._cache: return self._cache[entity_id] diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 608fae0242e..ebde309de14 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -1,7 +1,9 @@ """Helper class to implement include/exclude of entities and domains.""" +from __future__ import annotations + import fnmatch import re -from typing import Callable, Dict, List, Pattern +from typing import Callable, Pattern import voluptuous as vol @@ -19,7 +21,7 @@ CONF_EXCLUDE_ENTITIES = "exclude_entities" CONF_ENTITY_GLOBS = "entity_globs" -def convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: +def convert_filter(config: dict[str, list[str]]) -> Callable[[str], bool]: """Convert the filter schema into a filter.""" filt = generate_filter( config[CONF_INCLUDE_DOMAINS], @@ -57,7 +59,7 @@ FILTER_SCHEMA = vol.All(BASE_FILTER_SCHEMA, convert_filter) def convert_include_exclude_filter( - config: Dict[str, Dict[str, List[str]]] + config: dict[str, dict[str, list[str]]] ) -> Callable[[str], bool]: """Convert the include exclude filter schema into a filter.""" include = config[CONF_INCLUDE] @@ -107,7 +109,7 @@ def _glob_to_re(glob: str) -> Pattern[str]: return re.compile(fnmatch.translate(glob)) -def _test_against_patterns(patterns: List[Pattern[str]], entity_id: str) -> bool: +def _test_against_patterns(patterns: list[Pattern[str]], entity_id: str) -> bool: """Test entity against list of patterns, true if any match.""" for pattern in patterns: if pattern.match(entity_id): @@ -119,12 +121,12 @@ def _test_against_patterns(patterns: List[Pattern[str]], entity_id: str) -> bool # It's safe since we don't modify it. And None causes typing warnings # pylint: disable=dangerous-default-value def generate_filter( - include_domains: List[str], - include_entities: List[str], - exclude_domains: List[str], - exclude_entities: List[str], - include_entity_globs: List[str] = [], - exclude_entity_globs: List[str] = [], + include_domains: list[str], + include_entities: list[str], + exclude_domains: list[str], + exclude_entities: list[str], + include_entity_globs: list[str] = [], + exclude_entity_globs: list[str] = [], ) -> Callable[[str], bool]: """Return a function that will filter entities based on the args.""" include_d = set(include_domains) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index f496c7088a4..2a3ee75ce75 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,4 +1,6 @@ """Helpers for listening to events.""" +from __future__ import annotations + import asyncio import copy from dataclasses import dataclass @@ -6,18 +8,7 @@ from datetime import datetime, timedelta import functools as ft import logging import time -from typing import ( - Any, - Awaitable, - Callable, - Dict, - Iterable, - List, - Optional, - Set, - Tuple, - Union, -) +from typing import Any, Awaitable, Callable, Iterable, List import attr @@ -79,8 +70,8 @@ class TrackStates: """ all_states: bool - entities: Set - domains: Set + entities: set + domains: set @dataclass @@ -94,7 +85,7 @@ class TrackTemplate: template: Template variables: TemplateVarsType - rate_limit: Optional[timedelta] = None + rate_limit: timedelta | None = None @dataclass @@ -146,10 +137,10 @@ def threaded_listener_factory( @bind_hass def async_track_state_change( hass: HomeAssistant, - entity_ids: Union[str, Iterable[str]], + entity_ids: str | Iterable[str], action: Callable[[str, State, State], None], - from_state: Union[None, str, Iterable[str]] = None, - to_state: Union[None, str, Iterable[str]] = None, + from_state: None | str | Iterable[str] = None, + to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: """Track specific state changes. @@ -240,7 +231,7 @@ track_state_change = threaded_listener_factory(async_track_state_change) @bind_hass def async_track_state_change_event( hass: HomeAssistant, - entity_ids: Union[str, Iterable[str]], + entity_ids: str | Iterable[str], action: Callable[[Event], Any], ) -> Callable[[], None]: """Track specific state change events indexed by entity_id. @@ -337,7 +328,7 @@ def _async_remove_indexed_listeners( @bind_hass def async_track_entity_registry_updated_event( hass: HomeAssistant, - entity_ids: Union[str, Iterable[str]], + entity_ids: str | Iterable[str], action: Callable[[Event], Any], ) -> Callable[[], None]: """Track specific entity registry updated events indexed by entity_id. @@ -402,7 +393,7 @@ def async_track_entity_registry_updated_event( @callback def _async_dispatch_domain_event( - hass: HomeAssistant, event: Event, callbacks: Dict[str, List] + hass: HomeAssistant, event: Event, callbacks: dict[str, list] ) -> None: domain = split_entity_id(event.data["entity_id"])[0] @@ -423,7 +414,7 @@ def _async_dispatch_domain_event( @bind_hass def async_track_state_added_domain( hass: HomeAssistant, - domains: Union[str, Iterable[str]], + domains: str | Iterable[str], action: Callable[[Event], Any], ) -> Callable[[], None]: """Track state change events when an entity is added to domains.""" @@ -476,7 +467,7 @@ def async_track_state_added_domain( @bind_hass def async_track_state_removed_domain( hass: HomeAssistant, - domains: Union[str, Iterable[str]], + domains: str | Iterable[str], action: Callable[[Event], Any], ) -> Callable[[], None]: """Track state change events when an entity is removed from domains.""" @@ -527,7 +518,7 @@ def async_track_state_removed_domain( @callback -def _async_string_to_lower_list(instr: Union[str, Iterable[str]]) -> List[str]: +def _async_string_to_lower_list(instr: str | Iterable[str]) -> list[str]: if isinstance(instr, str): return [instr.lower()] @@ -546,7 +537,7 @@ class _TrackStateChangeFiltered: """Handle removal / refresh of tracker init.""" self.hass = hass self._action = action - self._listeners: Dict[str, Callable] = {} + self._listeners: dict[str, Callable] = {} self._last_track_states: TrackStates = track_states @callback @@ -569,7 +560,7 @@ class _TrackStateChangeFiltered: self._setup_entities_listener(track_states.domains, track_states.entities) @property - def listeners(self) -> Dict: + def listeners(self) -> dict: """State changes that will cause a re-render.""" track_states = self._last_track_states return { @@ -628,7 +619,7 @@ class _TrackStateChangeFiltered: self._listeners.pop(listener_name)() @callback - def _setup_entities_listener(self, domains: Set, entities: Set) -> None: + def _setup_entities_listener(self, domains: set, entities: set) -> None: if domains: entities = entities.copy() entities.update(self.hass.states.async_entity_ids(domains)) @@ -642,7 +633,7 @@ class _TrackStateChangeFiltered: ) @callback - def _setup_domains_listener(self, domains: Set) -> None: + def _setup_domains_listener(self, domains: set) -> None: if not domains: return @@ -691,8 +682,8 @@ def async_track_state_change_filtered( def async_track_template( hass: HomeAssistant, template: Template, - action: Callable[[str, Optional[State], Optional[State]], None], - variables: Optional[TemplateVarsType] = None, + action: Callable[[str, State | None, State | None], None], + variables: TemplateVarsType | None = None, ) -> Callable[[], None]: """Add a listener that fires when a a template evaluates to 'true'. @@ -734,7 +725,7 @@ def async_track_template( @callback def _template_changed_listener( - event: Event, updates: List[TrackTemplateResult] + event: Event, updates: list[TrackTemplateResult] ) -> None: """Check if condition is correct and run action.""" track_result = updates.pop() @@ -792,12 +783,12 @@ class _TrackTemplateResultInfo: track_template_.template.hass = hass self._track_templates = track_templates - self._last_result: Dict[Template, Union[str, TemplateError]] = {} + self._last_result: dict[Template, str | TemplateError] = {} self._rate_limit = KeyedRateLimit(hass) - self._info: Dict[Template, RenderInfo] = {} - self._track_state_changes: Optional[_TrackStateChangeFiltered] = None - self._time_listeners: Dict[Template, Callable] = {} + self._info: dict[Template, RenderInfo] = {} + self._track_state_changes: _TrackStateChangeFiltered | None = None + self._time_listeners: dict[Template, Callable] = {} def async_setup(self, raise_on_template_error: bool) -> None: """Activation of template tracking.""" @@ -826,7 +817,7 @@ class _TrackTemplateResultInfo: ) @property - def listeners(self) -> Dict: + def listeners(self) -> dict: """State changes that will cause a re-render.""" assert self._track_state_changes return { @@ -882,8 +873,8 @@ class _TrackTemplateResultInfo: self, track_template_: TrackTemplate, now: datetime, - event: Optional[Event], - ) -> Union[bool, TrackTemplateResult]: + event: Event | None, + ) -> bool | TrackTemplateResult: """Re-render the template if conditions match. Returns False if the template was not be re-rendered @@ -927,7 +918,7 @@ class _TrackTemplateResultInfo: ) try: - result: Union[str, TemplateError] = info.result() + result: str | TemplateError = info.result() except TemplateError as ex: result = ex @@ -945,9 +936,9 @@ class _TrackTemplateResultInfo: @callback def _refresh( self, - event: Optional[Event], - track_templates: Optional[Iterable[TrackTemplate]] = None, - replayed: Optional[bool] = False, + event: Event | None, + track_templates: Iterable[TrackTemplate] | None = None, + replayed: bool | None = False, ) -> None: """Refresh the template. @@ -1076,16 +1067,16 @@ def async_track_same_state( hass: HomeAssistant, period: timedelta, action: Callable[..., None], - async_check_same_func: Callable[[str, Optional[State], Optional[State]], bool], - entity_ids: Union[str, Iterable[str]] = MATCH_ALL, + async_check_same_func: Callable[[str, State | None, State | None], bool], + entity_ids: str | Iterable[str] = MATCH_ALL, ) -> CALLBACK_TYPE: """Track the state of entities for a period and run an action. If async_check_func is None it use the state of orig_value. Without entity_ids we track all state changes. """ - async_remove_state_for_cancel: Optional[CALLBACK_TYPE] = None - async_remove_state_for_listener: Optional[CALLBACK_TYPE] = None + async_remove_state_for_cancel: CALLBACK_TYPE | None = None + async_remove_state_for_listener: CALLBACK_TYPE | None = None job = HassJob(action) @@ -1113,8 +1104,8 @@ def async_track_same_state( def state_for_cancel_listener(event: Event) -> None: """Fire on changes and cancel for listener if changed.""" entity: str = event.data["entity_id"] - from_state: Optional[State] = event.data.get("old_state") - to_state: Optional[State] = event.data.get("new_state") + from_state: State | None = event.data.get("old_state") + to_state: State | None = event.data.get("new_state") if not async_check_same_func(entity, from_state, to_state): clear_listener() @@ -1144,7 +1135,7 @@ track_same_state = threaded_listener_factory(async_track_same_state) @bind_hass def async_track_point_in_time( hass: HomeAssistant, - action: Union[HassJob, Callable[..., None]], + action: HassJob | Callable[..., None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" @@ -1165,7 +1156,7 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @bind_hass def async_track_point_in_utc_time( hass: HomeAssistant, - action: Union[HassJob, Callable[..., None]], + action: HassJob | Callable[..., None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" @@ -1176,7 +1167,7 @@ def async_track_point_in_utc_time( # having to figure out how to call the action every time its called. job = action if isinstance(action, HassJob) else HassJob(action) - cancel_callback: Optional[asyncio.TimerHandle] = None + cancel_callback: asyncio.TimerHandle | None = None @callback def run_action() -> None: @@ -1217,7 +1208,7 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim @callback @bind_hass def async_call_later( - hass: HomeAssistant, delay: float, action: Union[HassJob, Callable[..., None]] + hass: HomeAssistant, delay: float, action: HassJob | Callable[..., None] ) -> CALLBACK_TYPE: """Add a listener that is called in .""" return async_track_point_in_utc_time( @@ -1232,7 +1223,7 @@ call_later = threaded_listener_factory(async_call_later) @bind_hass def async_track_time_interval( hass: HomeAssistant, - action: Callable[..., Union[None, Awaitable]], + action: Callable[..., None | Awaitable], interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" @@ -1276,9 +1267,9 @@ class SunListener: hass: HomeAssistant = attr.ib() job: HassJob = attr.ib() event: str = attr.ib() - offset: Optional[timedelta] = attr.ib() - _unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None) - _unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None) + offset: timedelta | None = attr.ib() + _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None) + _unsub_config: CALLBACK_TYPE | None = attr.ib(default=None) @callback def async_attach(self) -> None: @@ -1332,7 +1323,7 @@ class SunListener: @callback @bind_hass def async_track_sunrise( - hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None + hass: HomeAssistant, action: Callable[..., None], offset: timedelta | None = None ) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunrise daily.""" listener = SunListener(hass, HassJob(action), SUN_EVENT_SUNRISE, offset) @@ -1346,7 +1337,7 @@ track_sunrise = threaded_listener_factory(async_track_sunrise) @callback @bind_hass def async_track_sunset( - hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None + hass: HomeAssistant, action: Callable[..., None], offset: timedelta | None = None ) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunset daily.""" listener = SunListener(hass, HassJob(action), SUN_EVENT_SUNSET, offset) @@ -1365,9 +1356,9 @@ time_tracker_utcnow = dt_util.utcnow def async_track_utc_time_change( hass: HomeAssistant, action: Callable[..., None], - hour: Optional[Any] = None, - minute: Optional[Any] = None, - second: Optional[Any] = None, + hour: Any | None = None, + minute: Any | None = None, + second: Any | None = None, local: bool = False, ) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" @@ -1394,7 +1385,7 @@ def async_track_utc_time_change( localized_now, matching_seconds, matching_minutes, matching_hours ) - time_listener: Optional[CALLBACK_TYPE] = None + time_listener: CALLBACK_TYPE | None = None @callback def pattern_time_change_listener(_: datetime) -> None: @@ -1431,9 +1422,9 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) def async_track_time_change( hass: HomeAssistant, action: Callable[..., None], - hour: Optional[Any] = None, - minute: Optional[Any] = None, - second: Optional[Any] = None, + hour: Any | None = None, + minute: Any | None = None, + second: Any | None = None, ) -> CALLBACK_TYPE: """Add a listener that will fire if UTC time matches a pattern.""" return async_track_utc_time_change(hass, action, hour, minute, second, local=True) @@ -1442,9 +1433,7 @@ def async_track_time_change( track_time_change = threaded_listener_factory(async_track_time_change) -def process_state_match( - parameter: Union[None, str, Iterable[str]] -) -> Callable[[str], bool]: +def process_state_match(parameter: None | str | Iterable[str]) -> Callable[[str], bool]: """Convert parameter to function that matches input against parameter.""" if parameter is None or parameter == MATCH_ALL: return lambda _: True @@ -1459,7 +1448,7 @@ def process_state_match( @callback def _entities_domains_from_render_infos( render_infos: Iterable[RenderInfo], -) -> Tuple[Set, Set]: +) -> tuple[set, set]: """Combine from multiple RenderInfo.""" entities = set() domains = set() @@ -1520,7 +1509,7 @@ def _event_triggers_rerender(event: Event, info: RenderInfo) -> bool: @callback def _rate_limit_for_event( event: Event, info: RenderInfo, track_template_: TrackTemplate -) -> Optional[timedelta]: +) -> timedelta | None: """Determine the rate limit for an event.""" entity_id = event.data.get(ATTR_ENTITY_ID) @@ -1532,7 +1521,7 @@ def _rate_limit_for_event( if track_template_.rate_limit is not None: return track_template_.rate_limit - rate_limit: Optional[timedelta] = info.rate_limit + rate_limit: timedelta | None = info.rate_limit return rate_limit diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index a0517338ec8..f10e8f4c25c 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -1,9 +1,11 @@ """Provide frame helper for finding the current frame context.""" +from __future__ import annotations + import asyncio import functools import logging from traceback import FrameSummary, extract_stack -from typing import Any, Callable, Optional, Tuple, TypeVar, cast +from typing import Any, Callable, TypeVar, cast from homeassistant.exceptions import HomeAssistantError @@ -13,8 +15,8 @@ CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-na def get_integration_frame( - exclude_integrations: Optional[set] = None, -) -> Tuple[FrameSummary, str, str]: + exclude_integrations: set | None = None, +) -> tuple[FrameSummary, str, str]: """Return the frame, integration and integration path of the current stack frame.""" found_frame = None if not exclude_integrations: @@ -64,7 +66,7 @@ def report(what: str) -> None: def report_integration( - what: str, integration_frame: Tuple[FrameSummary, str, str] + what: str, integration_frame: tuple[FrameSummary, str, str] ) -> None: """Report incorrect usage in an integration. diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index b86223964b3..cc4f5be47d8 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -1,13 +1,14 @@ """Helper for httpx.""" +from __future__ import annotations + import sys -from typing import Any, Callable, Optional +from typing import Any, Callable import httpx from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.frame import warn_use -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass DATA_ASYNC_CLIENT = "httpx_async_client" @@ -20,16 +21,14 @@ USER_AGENT = "User-Agent" @callback @bind_hass -def get_async_client( - hass: HomeAssistantType, verify_ssl: bool = True -) -> httpx.AsyncClient: +def get_async_client(hass: HomeAssistant, verify_ssl: bool = True) -> httpx.AsyncClient: """Return default httpx AsyncClient. This method must be run in the event loop. """ key = DATA_ASYNC_CLIENT if verify_ssl else DATA_ASYNC_CLIENT_NOVERIFY - client: Optional[httpx.AsyncClient] = hass.data.get(key) + client: httpx.AsyncClient | None = hass.data.get(key) if client is None: client = hass.data[key] = create_async_httpx_client(hass, verify_ssl) @@ -37,9 +36,20 @@ def get_async_client( return client +class HassHttpXAsyncClient(httpx.AsyncClient): + """httpx AsyncClient that suppresses context management.""" + + async def __aenter__(self: HassHttpXAsyncClient) -> HassHttpXAsyncClient: + """Prevent an integration from reopen of the client via context manager.""" + return self + + async def __aexit__(self, *args: Any) -> None: + """Prevent an integration from close of the client via context manager.""" + + @callback def create_async_httpx_client( - hass: HomeAssistantType, + hass: HomeAssistant, verify_ssl: bool = True, auto_cleanup: bool = True, **kwargs: Any, @@ -51,7 +61,7 @@ def create_async_httpx_client( This method must be run in the event loop. """ - client = httpx.AsyncClient( + client = HassHttpXAsyncClient( verify=verify_ssl, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, @@ -71,7 +81,7 @@ def create_async_httpx_client( @callback def _async_register_async_client_shutdown( - hass: HomeAssistantType, + hass: HomeAssistant, client: httpx.AsyncClient, original_aclose: Callable[..., Any], ) -> None: diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index dd64e9c92f1..628dc9d341e 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -1,9 +1,9 @@ """Icon helper methods.""" -from typing import Optional +from __future__ import annotations def icon_for_battery_level( - battery_level: Optional[int] = None, charging: bool = False + battery_level: int | None = None, charging: bool = False ) -> str: """Return a battery icon valid identifier.""" icon = "mdi:battery" @@ -20,7 +20,7 @@ def icon_for_battery_level( return icon -def icon_for_signal_level(signal_level: Optional[int] = None) -> str: +def icon_for_signal_level(signal_level: int | None = None) -> str: """Return a signal icon valid identifier.""" if signal_level is None or signal_level == 0: return "mdi:signal-cellular-outline" diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 5feca605099..5b6f645c55a 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -1,5 +1,6 @@ """Helper to create a unique instance ID.""" -from typing import Dict, Optional +from __future__ import annotations + import uuid from homeassistant.core import HomeAssistant @@ -17,7 +18,7 @@ async def async_get(hass: HomeAssistant) -> str: """Get unique ID for the hass instance.""" store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) - data: Optional[Dict[str, str]] = await storage.async_migrator( # type: ignore + data: dict[str, str] | None = await storage.async_migrator( # type: ignore hass, hass.config.path(LEGACY_UUID_FILE), store, diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 1c5d56ccbd1..6ed8a6b5968 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -3,15 +3,14 @@ from __future__ import annotations import logging import re -from typing import Any, Callable, Dict, Iterable, Optional +from typing import Any, Callable, Dict, Iterable import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES -from homeassistant.core import Context, State, T, callback +from homeassistant.core import Context, HomeAssistant, State, T, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -31,7 +30,7 @@ SPEECH_TYPE_SSML = "ssml" @callback @bind_hass -def async_register(hass: HomeAssistantType, handler: IntentHandler) -> None: +def async_register(hass: HomeAssistant, handler: IntentHandler) -> None: """Register an intent with Home Assistant.""" intents = hass.data.get(DATA_KEY) if intents is None: @@ -49,12 +48,12 @@ def async_register(hass: HomeAssistantType, handler: IntentHandler) -> None: @bind_hass async def async_handle( - hass: HomeAssistantType, + hass: HomeAssistant, platform: str, intent_type: str, - slots: Optional[_SlotsType] = None, - text_input: Optional[str] = None, - context: Optional[Context] = None, + slots: _SlotsType | None = None, + text_input: str | None = None, + context: Context | None = None, ) -> IntentResponse: """Handle an intent.""" handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -103,7 +102,7 @@ class IntentUnexpectedError(IntentError): @callback @bind_hass def async_match_state( - hass: HomeAssistantType, name: str, states: Optional[Iterable[State]] = None + hass: HomeAssistant, name: str, states: Iterable[State] | None = None ) -> State: """Find a state that matches the name.""" if states is None: @@ -127,10 +126,10 @@ def async_test_feature(state: State, feature: int, feature_name: str) -> None: class IntentHandler: """Intent handler registration.""" - intent_type: Optional[str] = None - slot_schema: Optional[vol.Schema] = None - _slot_schema: Optional[vol.Schema] = None - platforms: Optional[Iterable[str]] = [] + intent_type: str | None = None + slot_schema: vol.Schema | None = None + _slot_schema: vol.Schema | None = None + platforms: Iterable[str] | None = [] @callback def async_can_handle(self, intent_obj: Intent) -> bool: @@ -163,7 +162,7 @@ class IntentHandler: return f"<{self.__class__.__name__} - {self.intent_type}>" -def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> Optional[T]: +def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> T | None: """Fuzzy matching function.""" matches = [] pattern = ".*?".join(name) @@ -222,11 +221,11 @@ class Intent: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, platform: str, intent_type: str, slots: _SlotsType, - text_input: Optional[str], + text_input: str | None, context: Context, ) -> None: """Initialize an intent.""" @@ -246,15 +245,15 @@ class Intent: class IntentResponse: """Response to an intent.""" - def __init__(self, intent: Optional[Intent] = None) -> None: + def __init__(self, intent: Intent | None = None) -> None: """Initialize an IntentResponse.""" self.intent = intent - self.speech: Dict[str, Dict[str, Any]] = {} - self.card: Dict[str, Dict[str, str]] = {} + self.speech: dict[str, dict[str, Any]] = {} + self.card: dict[str, dict[str, str]] = {} @callback def async_set_speech( - self, speech: str, speech_type: str = "plain", extra_data: Optional[Any] = None + self, speech: str, speech_type: str = "plain", extra_data: Any | None = None ) -> None: """Set speech response.""" self.speech[speech_type] = {"speech": speech, "extra_data": extra_data} @@ -267,6 +266,6 @@ class IntentResponse: self.card[card_type] = {"title": title, "content": content} @callback - def as_dict(self) -> Dict[str, Dict[str, Dict[str, Any]]]: + def as_dict(self) -> dict[str, dict[str, dict[str, Any]]]: """Return a dictionary representation of an intent response.""" return {"speech": self.speech, "card": self.card} diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 19058bc3e7f..a613220ef0f 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -1,13 +1,13 @@ """Location helpers for Home Assistant.""" +from __future__ import annotations import logging -from typing import Optional, Sequence +from typing import Sequence import voluptuous as vol from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.core import State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, State from homeassistant.util import location as loc_util _LOGGER = logging.getLogger(__name__) @@ -25,9 +25,7 @@ def has_location(state: State) -> bool: ) -def closest( - latitude: float, longitude: float, states: Sequence[State] -) -> Optional[State]: +def closest(latitude: float, longitude: float, states: Sequence[State]) -> State | None: """Return closest state to point. Async friendly. @@ -50,8 +48,8 @@ def closest( def find_coordinates( - hass: HomeAssistantType, entity_id: str, recursion_history: Optional[list] = None -) -> Optional[str]: + hass: HomeAssistant, entity_id: str, recursion_history: list | None = None +) -> str | None: """Find the gps coordinates of the entity in the form of '90.000,180.000'.""" entity_state = hass.states.get(entity_id) diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index 49b9bfcffec..a32d13ce513 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -1,7 +1,9 @@ """Helpers for logging allowing more advanced logging styles to be used.""" +from __future__ import annotations + import inspect import logging -from typing import Any, Mapping, MutableMapping, Optional, Tuple +from typing import Any, Mapping, MutableMapping class KeywordMessage: @@ -26,7 +28,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): """Represents an adapter wrapping the logger allowing KeywordMessages.""" def __init__( - self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] = None + self, logger: logging.Logger, extra: Mapping[str, Any] | None = None ) -> None: """Initialize a new StyleAdapter for the provided logger.""" super().__init__(logger, extra or {}) @@ -41,7 +43,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): def process( self, msg: Any, kwargs: MutableMapping[str, Any] - ) -> Tuple[Any, MutableMapping[str, Any]]: + ) -> tuple[Any, MutableMapping[str, Any]]: """Process the keyword args in preparation for logging.""" return ( msg, diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 21f69dc539a..6ed8084413f 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,9 @@ """Network helpers.""" +from __future__ import annotations + +from contextlib import suppress from ipaddress import ip_address -from typing import Optional, cast +from typing import cast import yarl @@ -54,7 +57,7 @@ def get_url( for url_type in order: if allow_internal and url_type == TYPE_URL_INTERNAL: - try: + with suppress(NoURLAvailableError): return _get_internal_url( hass, allow_ip=allow_ip, @@ -62,11 +65,9 @@ def get_url( require_ssl=require_ssl, require_standard_port=require_standard_port, ) - except NoURLAvailableError: - pass if allow_external and url_type == TYPE_URL_EXTERNAL: - try: + with suppress(NoURLAvailableError): return _get_external_url( hass, allow_cloud=allow_cloud, @@ -76,8 +77,6 @@ def get_url( require_ssl=require_ssl, require_standard_port=require_standard_port, ) - except NoURLAvailableError: - pass # For current request, we accept loopback interfaces (e.g., 127.0.0.1), # the Supervisor hostname and localhost transparently @@ -117,7 +116,7 @@ def get_url( raise NoURLAvailableError -def _get_request_host() -> Optional[str]: +def _get_request_host() -> str | None: """Get the host address of the current request.""" request = http.current_request.get() if request is None: @@ -175,10 +174,8 @@ def _get_external_url( ) -> str: """Get external URL of this instance.""" if prefer_cloud and allow_cloud: - try: + with suppress(NoURLAvailableError): return _get_cloud_url(hass) - except NoURLAvailableError: - pass if hass.config.external_url: external_url = yarl.URL(hass.config.external_url) @@ -199,10 +196,8 @@ def _get_external_url( return normalize_url(str(external_url)) if allow_cloud: - try: + with suppress(NoURLAvailableError): return _get_cloud_url(hass, require_current_request=require_current_request) - except NoURLAvailableError: - pass raise NoURLAvailableError diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index 40f10e69d25..fa671c6627f 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -1,8 +1,10 @@ """Ratelimit helper.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Callable, Dict, Hashable, Optional +from typing import Any, Callable, Hashable from homeassistant.core import HomeAssistant, callback import homeassistant.util.dt as dt_util @@ -19,8 +21,8 @@ class KeyedRateLimit: ): """Initialize ratelimit tracker.""" self.hass = hass - self._last_triggered: Dict[Hashable, datetime] = {} - self._rate_limit_timers: Dict[Hashable, asyncio.TimerHandle] = {} + self._last_triggered: dict[Hashable, datetime] = {} + self._rate_limit_timers: dict[Hashable, asyncio.TimerHandle] = {} @callback def async_has_timer(self, key: Hashable) -> bool: @@ -30,7 +32,7 @@ class KeyedRateLimit: return key in self._rate_limit_timers @callback - def async_triggered(self, key: Hashable, now: Optional[datetime] = None) -> None: + def async_triggered(self, key: Hashable, now: datetime | None = None) -> None: """Call when the action we are tracking was triggered.""" self.async_cancel_timer(key) self._last_triggered[key] = now or dt_util.utcnow() @@ -54,11 +56,11 @@ class KeyedRateLimit: def async_schedule_action( self, key: Hashable, - rate_limit: Optional[timedelta], + rate_limit: timedelta | None, now: datetime, action: Callable, *args: Any, - ) -> Optional[datetime]: + ) -> datetime | None: """Check rate limits and schedule an action if we hit the limit. If the rate limit is hit: diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 4a768a79320..ef1d033cfa7 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -1,16 +1,17 @@ """Class to reload platforms.""" +from __future__ import annotations import asyncio import logging -from typing import Dict, Iterable, List, Optional +from typing import Iterable from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component @@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) async def async_reload_integration_platforms( - hass: HomeAssistantType, integration_name: str, integration_platforms: Iterable + hass: HomeAssistant, integration_name: str, integration_platforms: Iterable ) -> None: """Reload an integration's platforms. @@ -46,7 +47,7 @@ async def async_reload_integration_platforms( async def _resetup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, integration_name: str, integration_platform: str, unprocessed_conf: ConfigType, @@ -61,7 +62,7 @@ async def _resetup_platform( if not conf: return - root_config: Dict = {integration_platform: []} + root_config: dict = {integration_platform: []} # Extract only the config for template, ignore the rest. for p_type, p_config in config_per_platform(conf, integration_platform): if p_type != integration_name: @@ -98,10 +99,10 @@ async def _resetup_platform( async def _async_setup_platform( - hass: HomeAssistantType, + hass: HomeAssistant, integration_name: str, integration_platform: str, - platform_configs: List[Dict], + platform_configs: list[dict], ) -> None: """Platform for the first time when new configuration is added.""" if integration_platform not in hass.data: @@ -119,7 +120,7 @@ async def _async_setup_platform( async def _async_reconfig_platform( - platform: EntityPlatform, platform_configs: List[Dict] + platform: EntityPlatform, platform_configs: list[dict] ) -> None: """Reconfigure an already loaded platform.""" await platform.async_reset() @@ -128,8 +129,8 @@ async def _async_reconfig_platform( async def async_integration_yaml_config( - hass: HomeAssistantType, integration_name: str -) -> Optional[ConfigType]: + hass: HomeAssistant, integration_name: str +) -> ConfigType | None: """Fetch the latest yaml configuration for an integration.""" integration = await async_get_integration(hass, integration_name) @@ -140,8 +141,8 @@ async def async_integration_yaml_config( @callback def async_get_platform_without_config_entry( - hass: HomeAssistantType, integration_name: str, integration_platform_name: str -) -> Optional[EntityPlatform]: + hass: HomeAssistant, integration_name: str, integration_platform_name: str +) -> EntityPlatform | None: """Find an existing platform that is not a config entry.""" for integration_platform in async_get_platforms(hass, integration_name): if integration_platform.config_entry is not None: @@ -154,7 +155,7 @@ def async_get_platform_without_config_entry( async def async_setup_reload_service( - hass: HomeAssistantType, domain: str, platforms: Iterable + hass: HomeAssistant, domain: str, platforms: Iterable ) -> None: """Create the reload service for the domain.""" if hass.services.has_service(domain, SERVICE_RELOAD): @@ -170,9 +171,7 @@ async def async_setup_reload_service( ) -def setup_reload_service( - hass: HomeAssistantType, domain: str, platforms: Iterable -) -> None: +def setup_reload_service(hass: HomeAssistant, domain: str, platforms: Iterable) -> None: """Sync version of async_setup_reload_service.""" asyncio.run_coroutine_threadsafe( async_setup_reload_service(hass, domain, platforms), diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 4f738887ce3..3350ed7a073 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Dict, List, Optional, Set, cast +from typing import Any, cast from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( @@ -45,12 +45,12 @@ class StoredState: self.state = state self.last_seen = last_seen - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: """Return a dict representation of the stored state.""" return {"state": self.state.as_dict(), "last_seen": self.last_seen} @classmethod - def from_dict(cls, json_dict: Dict) -> StoredState: + def from_dict(cls, json_dict: dict) -> StoredState: """Initialize a stored state from a dict.""" last_seen = json_dict["last_seen"] @@ -106,11 +106,11 @@ class RestoreStateData: self.store: Store = Store( hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder ) - self.last_states: Dict[str, StoredState] = {} - self.entity_ids: Set[str] = set() + self.last_states: dict[str, StoredState] = {} + self.entity_ids: set[str] = set() @callback - def async_get_stored_states(self) -> List[StoredState]: + def async_get_stored_states(self) -> list[StoredState]: """Get the set of states which should be stored. This includes the states of all registered entities, as well as the @@ -235,7 +235,6 @@ class RestoreEntity(Entity): async def async_internal_added_to_hass(self) -> None: """Register this entity as a restorable entity.""" - assert self.hass is not None _, data = await asyncio.gather( super().async_internal_added_to_hass(), RestoreStateData.async_get_instance(self.hass), @@ -244,18 +243,17 @@ class RestoreEntity(Entity): async def async_internal_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" - assert self.hass is not None _, data = await asyncio.gather( super().async_internal_will_remove_from_hass(), RestoreStateData.async_get_instance(self.hass), ) data.async_restore_entity_removed(self.entity_id) - async def async_get_last_state(self) -> Optional[State]: + async def async_get_last_state(self) -> State | None: """Get the entity state from the previous run.""" if self.hass is None or self.entity_id is None: # Return None if this entity isn't added to hass yet - _LOGGER.warning("Cannot get last state. Entity not added to hass") + _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable] return None data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e4eb0d4a901..bf52fc81b6a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,22 +1,14 @@ """Helpers to execute scripts.""" +from __future__ import annotations + import asyncio +from contextlib import asynccontextmanager, suppress from datetime import datetime, timedelta from functools import partial import itertools import logging from types import MappingProxyType -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Sequence, - Set, - Tuple, - Union, - cast, -) +from typing import Any, Callable, Dict, Sequence, Union, cast import async_timeout import voluptuous as vol @@ -25,6 +17,7 @@ from homeassistant import exceptions from homeassistant.components import device_automation, scene from homeassistant.components.logger import LOGSEVERITY from homeassistant.const import ( + ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_ALIAS, @@ -63,8 +56,14 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import condition, config_validation as cv, service, template +from homeassistant.helpers.condition import trace_condition_function +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables +from homeassistant.helpers.trace import script_execution_set from homeassistant.helpers.trigger import ( async_initialize_triggers, async_validate_trigger_config, @@ -73,6 +72,19 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from homeassistant.util.dt import utcnow +from .trace import ( + TraceElement, + async_trace_path, + trace_append_element, + trace_id_get, + trace_path, + trace_path_get, + trace_set_result, + trace_stack_cv, + trace_stack_pop, + trace_stack_push, +) + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs SCRIPT_MODE_PARALLEL = "parallel" @@ -96,9 +108,11 @@ DEFAULT_MAX_EXCEEDED = "WARNING" ATTR_CUR = "current" ATTR_MAX = "max" -ATTR_MODE = "mode" DATA_SCRIPTS = "helpers.script" +DATA_SCRIPT_BREAKPOINTS = "helpers.script_breakpoints" +RUN_ID_ANY = "*" +NODE_ANY = "*" _LOGGER = logging.getLogger(__name__) @@ -108,6 +122,80 @@ _TIMEOUT_MSG = "Timeout reached, abort script." _SHUTDOWN_MAX_WAIT = 60 +ACTION_TRACE_NODE_MAX_LEN = 20 # Max length of a trace node for repeated actions + +SCRIPT_BREAKPOINT_HIT = "script_breakpoint_hit" +SCRIPT_DEBUG_CONTINUE_STOP = "script_debug_continue_stop_{}_{}" +SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all" + + +def action_trace_append(variables, path): + """Append a TraceElement to trace[path].""" + trace_element = TraceElement(variables, path) + trace_append_element(trace_element, ACTION_TRACE_NODE_MAX_LEN) + return trace_element + + +@asynccontextmanager +async def trace_action(hass, script_run, stop, variables): + """Trace action execution.""" + path = trace_path_get() + trace_element = action_trace_append(variables, path) + trace_stack_push(trace_stack_cv, trace_element) + + trace_id = trace_id_get() + if trace_id: + key = trace_id[0] + run_id = trace_id[1] + breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS] + if key in breakpoints and ( + ( + run_id in breakpoints[key] + and ( + path in breakpoints[key][run_id] + or NODE_ANY in breakpoints[key][run_id] + ) + ) + or ( + RUN_ID_ANY in breakpoints[key] + and ( + path in breakpoints[key][RUN_ID_ANY] + or NODE_ANY in breakpoints[key][RUN_ID_ANY] + ) + ) + ): + async_dispatcher_send(hass, SCRIPT_BREAKPOINT_HIT, key, run_id, path) + + done = asyncio.Event() + + @callback + def async_continue_stop(command=None): + if command == "stop": + stop.set() + done.set() + + signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) + remove_signal1 = async_dispatcher_connect(hass, signal, async_continue_stop) + remove_signal2 = async_dispatcher_connect( + hass, SCRIPT_DEBUG_CONTINUE_ALL, async_continue_stop + ) + + tasks = [hass.async_create_task(flag.wait()) for flag in (stop, done)] + await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in tasks: + task.cancel() + remove_signal1() + remove_signal2() + + try: + yield trace_element + except Exception as ex: + trace_element.set_error(ex) + raise ex + finally: + trace_stack_pop(trace_stack_cv) + + def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA): """Make a schema for a component that uses the script helper.""" return vol.Schema( @@ -138,8 +226,8 @@ STATIC_VALIDATION_ACTION_TYPES = ( async def async_validate_actions_config( - hass: HomeAssistant, actions: List[ConfigType] -) -> List[ConfigType]: + hass: HomeAssistant, actions: list[ConfigType] +) -> list[ConfigType]: """Validate a list of actions.""" return await asyncio.gather( *[async_validate_action_config(hass, action) for action in actions] @@ -205,9 +293,9 @@ class _ScriptRun: def __init__( self, hass: HomeAssistant, - script: "Script", - variables: Dict[str, Any], - context: Optional[Context], + script: Script, + variables: dict[str, Any], + context: Context | None, log_exceptions: bool, ) -> None: self._hass = hass @@ -216,7 +304,7 @@ class _ScriptRun: self._context = context self._log_exceptions = log_exceptions self._step = -1 - self._action: Optional[Dict[str, Any]] = None + self._action: dict[str, Any] | None = None self._stop = asyncio.Event() self._stopped = asyncio.Event() @@ -245,29 +333,36 @@ class _ScriptRun: async def async_run(self) -> None: """Run script.""" try: - if self._stop.is_set(): - return self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): if self._stop.is_set(): + script_execution_set("cancelled") break await self._async_step(log_exceptions=False) + else: + script_execution_set("finished") except _StopScript: - pass + script_execution_set("aborted") + except Exception: + script_execution_set("error") + raise finally: self._finish() async def _async_step(self, log_exceptions): - try: - await getattr( - self, f"_async_{cv.determine_script_action(self._action)}_step" - )() - except Exception as ex: - if not isinstance(ex, (_StopScript, asyncio.CancelledError)) and ( - self._log_exceptions or log_exceptions - ): - self._log_exception(ex) - raise + with trace_path(str(self._step)): + async with trace_action(self._hass, self, self._stop, self._variables): + if self._stop.is_set(): + return + try: + handler = f"_async_{cv.determine_script_action(self._action)}_step" + await getattr(self, handler)() + except Exception as ex: + if not isinstance(ex, _StopScript) and ( + self._log_exceptions or log_exceptions + ): + self._log_exception(ex) + raise def _finish(self) -> None: self._script._runs.remove(self) # pylint: disable=protected-access @@ -338,11 +433,12 @@ class _ScriptRun: delay = delay.total_seconds() self._changed() + trace_set_result(delay=delay, done=False) try: async with async_timeout.timeout(delay): await self._stop.wait() except asyncio.TimeoutError: - pass + trace_set_result(delay=delay, done=True) async def _async_wait_template_step(self): """Handle a wait template.""" @@ -354,6 +450,7 @@ class _ScriptRun: self._step_log("wait template", timeout) self._variables["wait"] = {"remaining": timeout, "completed": False} + trace_set_result(wait=self._variables["wait"]) wait_template = self._action[CONF_WAIT_TEMPLATE] wait_template.hass = self._hass @@ -366,10 +463,9 @@ class _ScriptRun: @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" - self._variables["wait"] = { - "remaining": to_context.remaining if to_context else timeout, - "completed": True, - } + wait_var = self._variables["wait"] + wait_var["remaining"] = to_context.remaining if to_context else timeout + wait_var["completed"] = True done.set() to_context = None @@ -386,10 +482,11 @@ class _ScriptRun: async with async_timeout.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: + self._variables["wait"]["remaining"] = 0.0 if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) + trace_set_result(wait=self._variables["wait"], timeout=True) raise _StopScript from ex - self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: task.cancel() @@ -401,10 +498,8 @@ class _ScriptRun: async def async_cancel_long_task() -> None: # Stop long task and wait for it to finish. long_task.cancel() - try: + with suppress(Exception): await long_task - except Exception: # pylint: disable=broad-except - pass # Wait for long task while monitoring for a stop request. stop_task = self._hass.async_create_task(self._stop.wait()) @@ -450,6 +545,7 @@ class _ScriptRun: else: limit = SERVICE_CALL_LIMIT + trace_set_result(params=params, running_script=running_script, limit=limit) service_task = self._hass.async_create_task( self._hass.services.async_call( **params, @@ -478,6 +574,7 @@ class _ScriptRun: async def _async_scene_step(self): """Activate the scene specified in the action.""" self._step_log("activate scene") + trace_set_result(scene=self._action[CONF_SCENE]) await self._hass.services.async_call( scene.DOMAIN, SERVICE_TURN_ON, @@ -503,6 +600,7 @@ class _ScriptRun: "Error rendering event data template: %s", ex, level=logging.ERROR ) + trace_set_result(event=self._action[CONF_EVENT], event_data=event_data) self._hass.bus.async_fire( self._action[CONF_EVENT], event_data, context=self._context ) @@ -514,15 +612,39 @@ class _ScriptRun: ) cond = await self._async_get_condition(self._action) try: - check = cond(self._hass, self._variables) + with trace_path("condition"): + check = cond(self._hass, self._variables) except exceptions.ConditionError as ex: _LOGGER.warning("Error in 'condition' evaluation:\n%s", ex) check = False self._log("Test condition %s: %s", self._script.last_action, check) + trace_set_result(result=check) if not check: raise _StopScript + def _test_conditions(self, conditions, name, condition_path=None): + if condition_path is None: + condition_path = name + + @trace_condition_function + def traced_test_conditions(hass, variables): + try: + with trace_path(condition_path): + for idx, cond in enumerate(conditions): + with trace_path(str(idx)): + if not cond(hass, variables): + return False + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex) + return None + + return True + + result = traced_test_conditions(self._hass, self._variables) + return result + + @async_trace_path("repeat") async def _async_repeat_step(self): """Repeat a sequence.""" description = self._action.get(CONF_ALIAS, "sequence") @@ -541,7 +663,8 @@ class _ScriptRun: async def async_run_sequence(iteration, extra_msg=""): self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg) - await self._async_run_script(script) + with trace_path("sequence"): + await self._async_run_script(script) if CONF_COUNT in repeat: count = repeat[CONF_COUNT] @@ -570,9 +693,9 @@ class _ScriptRun: for iteration in itertools.count(1): set_repeat_var(iteration) try: - if self._stop.is_set() or not all( - cond(self._hass, self._variables) for cond in conditions - ): + if self._stop.is_set(): + break + if not self._test_conditions(conditions, "while"): break except exceptions.ConditionError as ex: _LOGGER.warning("Error in 'while' evaluation:\n%s", ex) @@ -588,9 +711,9 @@ class _ScriptRun: set_repeat_var(iteration) await async_run_sequence(iteration) try: - if self._stop.is_set() or all( - cond(self._hass, self._variables) for cond in conditions - ): + if self._stop.is_set(): + break + if self._test_conditions(conditions, "until") in [True, None]: break except exceptions.ConditionError as ex: _LOGGER.warning("Error in 'until' evaluation:\n%s", ex) @@ -606,18 +729,22 @@ class _ScriptRun: # pylint: disable=protected-access choose_data = await self._script._async_get_choose_data(self._step) - for conditions, script in choose_data["choices"]: - try: - if all( - condition(self._hass, self._variables) for condition in conditions - ): - await self._async_run_script(script) - return - except exceptions.ConditionError as ex: - _LOGGER.warning("Error in 'choose' evaluation:\n%s", ex) + with trace_path("choose"): + for idx, (conditions, script) in enumerate(choose_data["choices"]): + with trace_path(str(idx)): + try: + if self._test_conditions(conditions, "choose", "conditions"): + trace_set_result(choice=idx) + with trace_path("sequence"): + await self._async_run_script(script) + return + except exceptions.ConditionError as ex: + _LOGGER.warning("Error in 'choose' evaluation:\n%s", ex) if choose_data["default"]: - await self._async_run_script(choose_data["default"]) + trace_set_result(choice="default") + with trace_path(["default"]): + await self._async_run_script(choose_data["default"]) async def _async_wait_for_trigger_step(self): """Wait for a trigger event.""" @@ -630,14 +757,14 @@ class _ScriptRun: variables = {**self._variables} self._variables["wait"] = {"remaining": timeout, "trigger": None} + trace_set_result(wait=self._variables["wait"]) done = asyncio.Event() async def async_done(variables, context=None): - self._variables["wait"] = { - "remaining": to_context.remaining if to_context else timeout, - "trigger": variables["trigger"], - } + wait_var = self._variables["wait"] + wait_var["remaining"] = to_context.remaining if to_context else timeout + wait_var["trigger"] = variables["trigger"] done.set() def log_cb(level, msg, **kwargs): @@ -664,10 +791,11 @@ class _ScriptRun: async with async_timeout.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: + self._variables["wait"]["remaining"] = 0.0 if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) + trace_set_result(wait=self._variables["wait"], timeout=True) raise _StopScript from ex - self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: task.cancel() @@ -767,7 +895,7 @@ async def _async_stop_scripts_at_shutdown(hass, event): _VarsType = Union[Dict[str, Any], MappingProxyType] -def _referenced_extract_ids(data: Dict[str, Any], key: str, found: Set[str]) -> None: +def _referenced_extract_ids(data: dict[str, Any], key: str, found: set[str]) -> None: """Extract referenced IDs.""" if not data: return @@ -778,10 +906,10 @@ def _referenced_extract_ids(data: Dict[str, Any], key: str, found: Set[str]) -> return if isinstance(item_ids, str): - item_ids = [item_ids] - - for item_id in item_ids: - found.add(item_id) + found.add(item_ids) + else: + for item_id in item_ids: + found.add(item_id) class Script: @@ -790,20 +918,20 @@ class Script: def __init__( self, hass: HomeAssistant, - sequence: Sequence[Dict[str, Any]], + sequence: Sequence[dict[str, Any]], name: str, domain: str, *, # Used in "Running " log message - running_description: Optional[str] = None, - change_listener: Optional[Callable[..., Any]] = None, + running_description: str | None = None, + change_listener: Callable[..., Any] | None = None, script_mode: str = DEFAULT_SCRIPT_MODE, max_runs: int = DEFAULT_MAX, max_exceeded: str = DEFAULT_MAX_EXCEEDED, - logger: Optional[logging.Logger] = None, + logger: logging.Logger | None = None, log_exceptions: bool = True, top_level: bool = True, - variables: Optional[ScriptVariables] = None, + variables: ScriptVariables | None = None, ) -> None: """Initialize the script.""" all_scripts = hass.data.get(DATA_SCRIPTS) @@ -817,6 +945,8 @@ class Script: all_scripts.append( {"instance": self, "started_before_shutdown": not hass.is_stopping} ) + if DATA_SCRIPT_BREAKPOINTS not in hass.data: + hass.data[DATA_SCRIPT_BREAKPOINTS] = {} self._hass = hass self.sequence = sequence @@ -834,25 +964,26 @@ class Script: self._log_exceptions = log_exceptions self.last_action = None - self.last_triggered: Optional[datetime] = None + self.last_triggered: datetime | None = None - self._runs: List[_ScriptRun] = [] + self._runs: list[_ScriptRun] = [] self.max_runs = max_runs self._max_exceeded = max_exceeded if script_mode == SCRIPT_MODE_QUEUED: self._queue_lck = asyncio.Lock() - self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} - self._repeat_script: Dict[int, Script] = {} - self._choose_data: Dict[int, Dict[str, Any]] = {} - self._referenced_entities: Optional[Set[str]] = None - self._referenced_devices: Optional[Set[str]] = None + self._config_cache: dict[set[tuple], Callable[..., bool]] = {} + self._repeat_script: dict[int, Script] = {} + self._choose_data: dict[int, dict[str, Any]] = {} + self._referenced_entities: set[str] | None = None + self._referenced_devices: set[str] | None = None + self._referenced_areas: set[str] | None = None self.variables = variables self._variables_dynamic = template.is_complex(variables) if self._variables_dynamic: template.attach(hass, variables) @property - def change_listener(self) -> Optional[Callable[..., Any]]: + def change_listener(self) -> Callable[..., Any] | None: """Return the change_listener.""" return self._change_listener @@ -866,13 +997,13 @@ class Script: ): self._change_listener_job = HassJob(change_listener) - def _set_logger(self, logger: Optional[logging.Logger] = None) -> None: + def _set_logger(self, logger: logging.Logger | None = None) -> None: if logger: self._logger = logger else: self._logger = logging.getLogger(f"{__name__}.{slugify(self.name)}") - def update_logger(self, logger: Optional[logging.Logger] = None) -> None: + def update_logger(self, logger: logging.Logger | None = None) -> None: """Update logger.""" self._set_logger(logger) for script in self._repeat_script.values(): @@ -908,19 +1039,40 @@ class Script: return self.script_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED) @property - def referenced_devices(self): - """Return a set of referenced devices.""" - if self._referenced_devices is not None: - return self._referenced_devices + def referenced_areas(self): + """Return a set of referenced areas.""" + if self._referenced_areas is not None: + return self._referenced_areas - referenced: Set[str] = set() + referenced: set[str] = set() + + for step in self.sequence: + action = cv.determine_script_action(step) + + if action == cv.SCRIPT_ACTION_CALL_SERVICE: + for data in ( + step.get(CONF_TARGET), + step.get(service.CONF_SERVICE_DATA), + step.get(service.CONF_SERVICE_DATA_TEMPLATE), + ): + _referenced_extract_ids(data, ATTR_AREA_ID, referenced) + + self._referenced_areas = referenced + return referenced + + @property + def referenced_devices(self): + """Return a set of referenced devices.""" + if self._referenced_devices is not None: + return self._referenced_devices + + referenced: set[str] = set() for step in self.sequence: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: for data in ( - step, step.get(CONF_TARGET), step.get(service.CONF_SERVICE_DATA), step.get(service.CONF_SERVICE_DATA_TEMPLATE), @@ -942,7 +1094,7 @@ class Script: if self._referenced_entities is not None: return self._referenced_entities - referenced: Set[str] = set() + referenced: set[str] = set() for step in self.sequence: action = cv.determine_script_action(step) @@ -966,7 +1118,7 @@ class Script: return referenced def run( - self, variables: Optional[_VarsType] = None, context: Optional[Context] = None + self, variables: _VarsType | None = None, context: Context | None = None ) -> None: """Run script.""" asyncio.run_coroutine_threadsafe( @@ -975,9 +1127,9 @@ class Script: async def async_run( self, - run_variables: Optional[_VarsType] = None, - context: Optional[Context] = None, - started_action: Optional[Callable[..., Any]] = None, + run_variables: _VarsType | None = None, + context: Context | None = None, + started_action: Callable[..., Any] | None = None, ) -> None: """Run script.""" if context is None: @@ -990,6 +1142,7 @@ class Script: if self.script_mode == SCRIPT_MODE_SINGLE: if self._max_exceeded != "SILENT": self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) + script_execution_set("failed_single") return if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") @@ -1000,6 +1153,7 @@ class Script: "Maximum number of runs exceeded", level=LOGSEVERITY[self._max_exceeded], ) + script_execution_set("failed_max_runs") return # If this is a top level Script then make a copy of the variables in case they @@ -1154,3 +1308,71 @@ class Script: self._logger.exception(msg, *args, **kwargs) else: self._logger.log(level, msg, *args, **kwargs) + + +@callback +def breakpoint_clear(hass, key, run_id, node): + """Clear a breakpoint.""" + run_id = run_id or RUN_ID_ANY + breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS] + if key not in breakpoints or run_id not in breakpoints[key]: + return + breakpoints[key][run_id].discard(node) + + +@callback +def breakpoint_clear_all(hass): + """Clear all breakpoints.""" + hass.data[DATA_SCRIPT_BREAKPOINTS] = {} + + +@callback +def breakpoint_set(hass, key, run_id, node): + """Set a breakpoint.""" + run_id = run_id or RUN_ID_ANY + breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS] + if key not in breakpoints: + breakpoints[key] = {} + if run_id not in breakpoints[key]: + breakpoints[key][run_id] = set() + breakpoints[key][run_id].add(node) + + +@callback +def breakpoint_list(hass): + """List breakpoints.""" + breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS] + + return [ + {"key": key, "run_id": run_id, "node": node} + for key in breakpoints + for run_id in breakpoints[key] + for node in breakpoints[key][run_id] + ] + + +@callback +def debug_continue(hass, key, run_id): + """Continue execution of a halted script.""" + # Clear any wildcard breakpoint + breakpoint_clear(hass, key, run_id, NODE_ANY) + + signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) + async_dispatcher_send(hass, signal, "continue") + + +@callback +def debug_step(hass, key, run_id): + """Single step a halted script.""" + # Set a wildcard breakpoint + breakpoint_set(hass, key, run_id, NODE_ANY) + + signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) + async_dispatcher_send(hass, signal, "continue") + + +@callback +def debug_stop(hass, key, run_id): + """Stop execution of a running or halted script.""" + signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) + async_dispatcher_send(hass, signal, "stop") diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 818263c9dd5..86a700bc62b 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -1,5 +1,7 @@ """Script variables.""" -from typing import Any, Dict, Mapping, Optional +from __future__ import annotations + +from typing import Any, Mapping from homeassistant.core import HomeAssistant, callback @@ -9,20 +11,20 @@ from . import template class ScriptVariables: """Class to hold and render script variables.""" - def __init__(self, variables: Dict[str, Any]): + def __init__(self, variables: dict[str, Any]): """Initialize script variables.""" self.variables = variables - self._has_template: Optional[bool] = None + self._has_template: bool | None = None @callback def async_render( self, hass: HomeAssistant, - run_variables: Optional[Mapping[str, Any]], + run_variables: Mapping[str, Any] | None, *, render_as_defaults: bool = True, limited: bool = False, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Render script variables. The run variables are used to compute the static variables. diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 34befe9c37b..99d871fc25b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1,4 +1,6 @@ """Selectors for Home Assistant.""" +from __future__ import annotations + from typing import Any, Callable, Dict, cast import voluptuous as vol @@ -9,7 +11,7 @@ from homeassistant.util import decorator SELECTORS = decorator.Registry() -def validate_selector(config: Any) -> Dict: +def validate_selector(config: Any) -> dict: """Validate a selector.""" if not isinstance(config, dict): raise vol.Invalid("Expected a dictionary") @@ -68,9 +70,7 @@ class DeviceSelector(Selector): # Model of device vol.Optional("model"): str, # Device has to contain entities matching this selector - vol.Optional( - "entity" - ): EntitySelector.CONFIG_SCHEMA, # pylint: disable=no-member + vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, } ) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 932384493f3..4e484c6aaab 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -5,21 +5,7 @@ import asyncio import dataclasses from functools import partial, wraps import logging -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Dict, - Iterable, - List, - Optional, - Set, - Tuple, - TypedDict, - Union, - cast, -) +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Iterable, TypedDict import voluptuous as vol @@ -36,7 +22,7 @@ from homeassistant.const import ( ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, ) -import homeassistant.core as ha +from homeassistant.core import Context, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ( HomeAssistantError, TemplateError, @@ -50,7 +36,7 @@ from homeassistant.helpers import ( entity_registry, template, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.loader import ( MAX_LOAD_CONCURRENTLY, Integration, @@ -62,7 +48,7 @@ from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE if TYPE_CHECKING: - from homeassistant.helpers.entity import Entity # noqa + from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import EntityPlatform @@ -79,8 +65,31 @@ class ServiceParams(TypedDict): domain: str service: str - service_data: Dict[str, Any] - target: Optional[Dict] + service_data: dict[str, Any] + target: dict | None + + +class ServiceTargetSelector: + """Class to hold a target selector for a service.""" + + def __init__(self, service_call: ServiceCall): + """Extract ids from service call data.""" + entity_ids: str | list | None = service_call.data.get(ATTR_ENTITY_ID) + device_ids: str | list | None = service_call.data.get(ATTR_DEVICE_ID) + area_ids: str | list | None = service_call.data.get(ATTR_AREA_ID) + + self.entity_ids = ( + set(cv.ensure_list(entity_ids)) if _has_match(entity_ids) else set() + ) + self.device_ids = ( + set(cv.ensure_list(device_ids)) if _has_match(device_ids) else set() + ) + self.area_ids = set(cv.ensure_list(area_ids)) if _has_match(area_ids) else set() + + @property + def has_any_selector(self) -> bool: + """Determine if any selectors are present.""" + return bool(self.entity_ids or self.device_ids or self.area_ids) @dataclasses.dataclass @@ -88,17 +97,20 @@ class SelectedEntities: """Class to hold the selected entities.""" # Entities that were explicitly mentioned. - referenced: Set[str] = dataclasses.field(default_factory=set) + referenced: set[str] = dataclasses.field(default_factory=set) # Entities that were referenced via device/area ID. # Should not trigger a warning when they don't exist. - indirectly_referenced: Set[str] = dataclasses.field(default_factory=set) + indirectly_referenced: set[str] = dataclasses.field(default_factory=set) # Referenced items that could not be found. - missing_devices: Set[str] = dataclasses.field(default_factory=set) - missing_areas: Set[str] = dataclasses.field(default_factory=set) + missing_devices: set[str] = dataclasses.field(default_factory=set) + missing_areas: set[str] = dataclasses.field(default_factory=set) - def log_missing(self, missing_entities: Set[str]) -> None: + # Referenced devices + referenced_devices: set[str] = dataclasses.field(default_factory=set) + + def log_missing(self, missing_entities: set[str]) -> None: """Log about missing items.""" parts = [] for label, items in ( @@ -117,7 +129,7 @@ class SelectedEntities: @bind_hass def call_from_config( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, blocking: bool = False, variables: TemplateVarsType = None, @@ -132,12 +144,12 @@ def call_from_config( @bind_hass async def async_call_from_config( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, blocking: bool = False, variables: TemplateVarsType = None, validate_config: bool = True, - context: Optional[ha.Context] = None, + context: Context | None = None, ) -> None: """Call a service based on a config hash.""" try: @@ -152,10 +164,10 @@ async def async_call_from_config( await hass.services.async_call(**params, blocking=blocking, context=context) -@ha.callback +@callback @bind_hass def async_prepare_call_from_config( - hass: HomeAssistantType, + hass: HomeAssistant, config: ConfigType, variables: TemplateVarsType = None, validate_config: bool = False, @@ -192,10 +204,15 @@ def async_prepare_call_from_config( target = {} if CONF_TARGET in config: - conf = config.get(CONF_TARGET) + conf = config[CONF_TARGET] try: - template.attach(hass, conf) - target.update(template.render_complex(conf, variables)) + if isinstance(conf, template.Template): + conf.hass = hass + target.update(conf.async_render(variables)) + else: + template.attach(hass, conf) + target.update(template.render_complex(conf, variables)) + if CONF_ENTITY_ID in target: target[CONF_ENTITY_ID] = cv.comp_entity_ids(target[CONF_ENTITY_ID]) except TemplateError as ex: @@ -234,8 +251,8 @@ def async_prepare_call_from_config( @bind_hass def extract_entity_ids( - hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True -) -> Set[str]: + hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True +) -> set[str]: """Extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. @@ -247,11 +264,11 @@ def extract_entity_ids( @bind_hass async def async_extract_entities( - hass: HomeAssistantType, + hass: HomeAssistant, entities: Iterable[Entity], - service_call: ha.ServiceCall, + service_call: ServiceCall, expand_group: bool = True, -) -> List[Entity]: +) -> list[Entity]: """Extract a list of entity objects from a service call. Will convert group entity ids to the entity ids it represents. @@ -286,8 +303,8 @@ async def async_extract_entities( @bind_hass async def async_extract_entity_ids( - hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True -) -> Set[str]: + hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True +) -> set[str]: """Extract a set of entity ids from a service call. Will convert group entity ids to the entity ids it represents. @@ -298,99 +315,89 @@ async def async_extract_entity_ids( return referenced.referenced | referenced.indirectly_referenced +def _has_match(ids: str | list | None) -> bool: + """Check if ids can match anything.""" + return ids not in (None, ENTITY_MATCH_NONE) + + @bind_hass async def async_extract_referenced_entity_ids( - hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True + hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True ) -> SelectedEntities: """Extract referenced entity IDs from a service call.""" - entity_ids = service_call.data.get(ATTR_ENTITY_ID) - device_ids = service_call.data.get(ATTR_DEVICE_ID) - area_ids = service_call.data.get(ATTR_AREA_ID) - - selects_entity_ids = entity_ids not in (None, ENTITY_MATCH_NONE) - selects_device_ids = device_ids not in (None, ENTITY_MATCH_NONE) - selects_area_ids = area_ids not in (None, ENTITY_MATCH_NONE) - + selector = ServiceTargetSelector(service_call) selected = SelectedEntities() - if not selects_entity_ids and not selects_device_ids and not selects_area_ids: + if not selector.has_any_selector: return selected - if selects_entity_ids: - assert entity_ids is not None + entity_ids = selector.entity_ids + if expand_group: + entity_ids = hass.components.group.expand_entity_ids(entity_ids) - # Entity ID attr can be a list or a string - if isinstance(entity_ids, str): - entity_ids = [entity_ids] + selected.referenced.update(entity_ids) - if expand_group: - entity_ids = hass.components.group.expand_entity_ids(entity_ids) - - selected.referenced.update(entity_ids) - - if not selects_device_ids and not selects_area_ids: + if not selector.device_ids and not selector.area_ids: return selected - area_reg, dev_reg, ent_reg = cast( - Tuple[ - area_registry.AreaRegistry, - device_registry.DeviceRegistry, - entity_registry.EntityRegistry, - ], - await asyncio.gather( - area_registry.async_get_registry(hass), - device_registry.async_get_registry(hass), - entity_registry.async_get_registry(hass), - ), - ) + ent_reg = entity_registry.async_get(hass) + dev_reg = device_registry.async_get(hass) + area_reg = area_registry.async_get(hass) - picked_devices = set() + for device_id in selector.device_ids: + if device_id not in dev_reg.devices: + selected.missing_devices.add(device_id) - if selects_device_ids: - if isinstance(device_ids, str): - picked_devices = {device_ids} - else: - assert isinstance(device_ids, list) - picked_devices = set(device_ids) + for area_id in selector.area_ids: + if area_id not in area_reg.areas: + selected.missing_areas.add(area_id) - for device_id in picked_devices: - if device_id not in dev_reg.devices: - selected.missing_devices.add(device_id) + # Find devices for this area + selected.referenced_devices.update(selector.device_ids) + for device_entry in dev_reg.devices.values(): + if device_entry.area_id in selector.area_ids: + selected.referenced_devices.add(device_entry.id) - if selects_area_ids: - assert area_ids is not None - - if isinstance(area_ids, str): - area_lookup = {area_ids} - else: - area_lookup = set(area_ids) - - for area_id in area_lookup: - if area_id not in area_reg.areas: - selected.missing_areas.add(area_id) - continue - - # Find entities tied to an area - for entity_entry in ent_reg.entities.values(): - if entity_entry.area_id in area_lookup: - selected.indirectly_referenced.add(entity_entry.entity_id) - - # Find devices for this area - for device_entry in dev_reg.devices.values(): - if device_entry.area_id in area_lookup: - picked_devices.add(device_entry.id) - - if not picked_devices: + if not selector.area_ids and not selected.referenced_devices: return selected - for entity_entry in ent_reg.entities.values(): - if not entity_entry.area_id and entity_entry.device_id in picked_devices: - selected.indirectly_referenced.add(entity_entry.entity_id) + for ent_entry in ent_reg.entities.values(): + if ent_entry.area_id in selector.area_ids or ( + not ent_entry.area_id and ent_entry.device_id in selected.referenced_devices + ): + selected.indirectly_referenced.add(ent_entry.entity_id) return selected -def _load_services_file(hass: HomeAssistantType, integration: Integration) -> JSON_TYPE: +@bind_hass +async def async_extract_config_entry_ids( + hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True +) -> set: + """Extract referenced config entry ids from a service call.""" + referenced = await async_extract_referenced_entity_ids( + hass, service_call, expand_group + ) + ent_reg = entity_registry.async_get(hass) + dev_reg = device_registry.async_get(hass) + config_entry_ids: set[str] = set() + + # Some devices may have no entities + for device_id in referenced.referenced_devices: + if device_id in dev_reg.devices: + device = dev_reg.async_get(device_id) + if device is not None: + config_entry_ids.update(device.config_entries) + + for entity_id in referenced.referenced | referenced.indirectly_referenced: + entry = ent_reg.async_get(entity_id) + if entry is not None and entry.config_entry_id is not None: + config_entry_ids.add(entry.config_entry_id) + + return config_entry_ids + + +def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE: """Load services file for an integration.""" try: return load_yaml(str(integration.file_path / "services.yaml")) @@ -407,16 +414,16 @@ def _load_services_file(hass: HomeAssistantType, integration: Integration) -> JS def _load_services_files( - hass: HomeAssistantType, integrations: Iterable[Integration] -) -> List[JSON_TYPE]: + hass: HomeAssistant, integrations: Iterable[Integration] +) -> list[JSON_TYPE]: """Load service files for multiple intergrations.""" return [_load_services_file(hass, integration) for integration in integrations] @bind_hass async def async_get_all_descriptions( - hass: HomeAssistantType, -) -> Dict[str, Dict[str, Any]]: + hass: HomeAssistant, +) -> dict[str, dict[str, Any]]: """Return descriptions (i.e. user documentation) for all service calls.""" descriptions_cache = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) format_cache_key = "{}.{}".format @@ -448,7 +455,7 @@ async def async_get_all_descriptions( loaded[domain] = content # Build response - descriptions: Dict[str, Dict[str, Any]] = {} + descriptions: dict[str, dict[str, Any]] = {} for domain in services: descriptions[domain] = {} @@ -480,10 +487,10 @@ async def async_get_all_descriptions( return descriptions -@ha.callback +@callback @bind_hass def async_set_service_schema( - hass: HomeAssistantType, domain: str, service: str, schema: Dict[str, Any] + hass: HomeAssistant, domain: str, service: str, schema: dict[str, Any] ) -> None: """Register a description for a service.""" hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) @@ -502,11 +509,11 @@ def async_set_service_schema( @bind_hass async def entity_service_call( - hass: HomeAssistantType, - platforms: Iterable["EntityPlatform"], - func: Union[str, Callable[..., Any]], - call: ha.ServiceCall, - required_features: Optional[Iterable[int]] = None, + hass: HomeAssistant, + platforms: Iterable[EntityPlatform], + func: str | Callable[..., Any], + call: ServiceCall, + required_features: Iterable[int] | None = None, ) -> None: """Handle an entity service call. @@ -516,17 +523,17 @@ async def entity_service_call( user = await hass.auth.async_get_user(call.context.user_id) if user is None: raise UnknownUser(context=call.context) - entity_perms: Optional[ + entity_perms: None | ( Callable[[str, str], bool] - ] = user.permissions.check_entity + ) = user.permissions.check_entity else: entity_perms = None target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL if target_all_entities: - referenced: Optional[SelectedEntities] = None - all_referenced: Optional[Set[str]] = None + referenced: SelectedEntities | None = None + all_referenced: set[str] | None = None else: # A set of entities we're trying to target. referenced = await async_extract_referenced_entity_ids(hass, call, True) @@ -534,7 +541,7 @@ async def entity_service_call( # If the service function is a string, we'll pass it the service call data if isinstance(func, str): - data: Union[Dict, ha.ServiceCall] = { + data: dict | ServiceCall = { key: val for key, val in call.data.items() if key not in cv.ENTITY_SERVICE_FIELDS @@ -546,7 +553,7 @@ async def entity_service_call( # Check the permissions # A list with entities to call the service on. - entity_candidates: List["Entity"] = [] + entity_candidates: list[Entity] = [] if entity_perms is None: for platform in platforms: @@ -660,11 +667,11 @@ async def entity_service_call( async def _handle_entity_call( - hass: HomeAssistantType, + hass: HomeAssistant, entity: Entity, - func: Union[str, Callable[..., Any]], - data: Union[Dict, ha.ServiceCall], - context: ha.Context, + func: str | Callable[..., Any], + data: dict | ServiceCall, + context: Context, ) -> None: """Handle calling service method.""" entity.async_set_context(context) @@ -688,18 +695,18 @@ async def _handle_entity_call( @bind_hass -@ha.callback +@callback def async_register_admin_service( - hass: HomeAssistantType, + hass: HomeAssistant, domain: str, service: str, - service_func: Callable[[ha.ServiceCall], Optional[Awaitable]], + service_func: Callable[[ServiceCall], Awaitable | None], schema: vol.Schema = vol.Schema({}, extra=vol.PREVENT_EXTRA), ) -> None: """Register a service that requires admin access.""" @wraps(service_func) - async def admin_handler(call: ha.ServiceCall) -> None: + async def admin_handler(call: ServiceCall) -> None: if call.context.user_id: user = await hass.auth.async_get_user(call.context.user_id) if user is None: @@ -715,20 +722,20 @@ def async_register_admin_service( @bind_hass -@ha.callback +@callback def verify_domain_control( - hass: HomeAssistantType, domain: str -) -> Callable[[Callable[[ha.ServiceCall], Any]], Callable[[ha.ServiceCall], Any]]: + hass: HomeAssistant, domain: str +) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]: """Ensure permission to access any entity under domain in service call.""" def decorator( - service_handler: Callable[[ha.ServiceCall], Any] - ) -> Callable[[ha.ServiceCall], Any]: + service_handler: Callable[[ServiceCall], Any] + ) -> Callable[[ServiceCall], Any]: """Decorate.""" if not asyncio.iscoroutinefunction(service_handler): raise HomeAssistantError("Can only decorate async functions.") - async def check_permissions(call: ha.ServiceCall) -> Any: + async def check_permissions(call: ServiceCall) -> Any: """Check user permission and raise before call if unauthorized.""" if not call.context.user_id: return await service_handler(call) diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index a7be57693ba..b34df0075a3 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -29,7 +29,7 @@ The following cases will never be passed to your function: from __future__ import annotations from types import MappingProxyType -from typing import Any, Callable, Dict, Optional, Tuple, Union +from typing import Any, Callable, Optional, Union from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State, callback @@ -66,7 +66,7 @@ ExtraCheckTypeFunc = Callable[ async def create_checker( hass: HomeAssistant, _domain: str, - extra_significant_check: Optional[ExtraCheckTypeFunc] = None, + extra_significant_check: ExtraCheckTypeFunc | None = None, ) -> SignificantlyChangedChecker: """Create a significantly changed checker for a domain.""" await _initialize(hass) @@ -90,15 +90,15 @@ async def _initialize(hass: HomeAssistant) -> None: await async_process_integration_platforms(hass, PLATFORM, process_platform) -def either_one_none(val1: Optional[Any], val2: Optional[Any]) -> bool: +def either_one_none(val1: Any | None, val2: Any | None) -> bool: """Test if exactly one value is None.""" return (val1 is None and val2 is not None) or (val1 is not None and val2 is None) def check_numeric_changed( - val1: Optional[Union[int, float]], - val2: Optional[Union[int, float]], - change: Union[int, float], + val1: int | float | None, + val2: int | float | None, + change: int | float, ) -> bool: """Check if two numeric values have changed.""" if val1 is None and val2 is None: @@ -125,22 +125,22 @@ class SignificantlyChangedChecker: def __init__( self, hass: HomeAssistant, - extra_significant_check: Optional[ExtraCheckTypeFunc] = None, + extra_significant_check: ExtraCheckTypeFunc | None = None, ) -> None: """Test if an entity has significantly changed.""" self.hass = hass - self.last_approved_entities: Dict[str, Tuple[State, Any]] = {} + self.last_approved_entities: dict[str, tuple[State, Any]] = {} self.extra_significant_check = extra_significant_check @callback def async_is_significant_change( - self, new_state: State, *, extra_arg: Optional[Any] = None + self, new_state: State, *, extra_arg: Any | None = None ) -> bool: """Return if this was a significant change. Extra kwargs are passed to the extra significant checker. """ - old_data: Optional[Tuple[State, Any]] = self.last_approved_entities.get( + old_data: tuple[State, Any] | None = self.last_approved_entities.get( new_state.entity_id ) @@ -164,9 +164,7 @@ class SignificantlyChangedChecker: self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg) return True - functions: Optional[Dict[str, CheckTypeFunc]] = self.hass.data.get( - DATA_FUNCTIONS - ) + functions: dict[str, CheckTypeFunc] | None = self.hass.data.get(DATA_FUNCTIONS) if functions is None: raise RuntimeError("Significant Change not initialized") diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index ab4d12dc1cc..a48ea5d64f0 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -1,7 +1,9 @@ """Helper to help coordinating calls.""" +from __future__ import annotations + import asyncio import functools -from typing import Callable, Optional, TypeVar, cast +from typing import Callable, TypeVar, cast from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass @@ -24,7 +26,7 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @bind_hass @functools.wraps(func) def wrapped(hass: HomeAssistant) -> T: - obj: Optional[T] = hass.data.get(data_key) + obj: T | None = hass.data.get(data_key) if obj is None: obj = hass.data[data_key] = func(hass) return obj diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 87112cd9133..c9f267a89f5 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,10 +1,12 @@ """Helpers that help with state related things.""" +from __future__ import annotations + import asyncio from collections import defaultdict import datetime as dt import logging from types import ModuleType, TracebackType -from typing import Any, Dict, Iterable, List, Optional, Type, Union +from typing import Any, Iterable from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON from homeassistant.const import ( @@ -18,11 +20,11 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, ) -from homeassistant.core import Context, State +from homeassistant.core import Context, HomeAssistant, State from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass import homeassistant.util.dt as dt_util -from .typing import HomeAssistantType +from .frame import report _LOGGER = logging.getLogger(__name__) @@ -35,24 +37,27 @@ class AsyncTrackStates: when with-block is exited. Must be run within the event loop. + + Deprecated. Remove after June 2021. + Warning added via `get_changed_since`. """ - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize a TrackStates block.""" self.hass = hass - self.states: List[State] = [] + self.states: list[State] = [] # pylint: disable=attribute-defined-outside-init - def __enter__(self) -> List[State]: + def __enter__(self) -> list[State]: """Record time from which to track changes.""" self.now = dt_util.utcnow() return self.states def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: """Add changes states to changes list.""" self.states.extend(get_changed_since(self.hass.states.async_all(), self.now)) @@ -60,29 +65,33 @@ class AsyncTrackStates: def get_changed_since( states: Iterable[State], utc_point_in_time: dt.datetime -) -> List[State]: - """Return list of states that have been changed since utc_point_in_time.""" +) -> list[State]: + """Return list of states that have been changed since utc_point_in_time. + + Deprecated. Remove after June 2021. + """ + report("uses deprecated `get_changed_since`") return [state for state in states if state.last_updated >= utc_point_in_time] @bind_hass async def async_reproduce_state( - hass: HomeAssistantType, - states: Union[State, Iterable[State]], + hass: HomeAssistant, + states: State | Iterable[State], *, - context: Optional[Context] = None, - reproduce_options: Optional[Dict[str, Any]] = None, + context: Context | None = None, + reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a list of states on multiple domains.""" if isinstance(states, State): states = [states] - to_call: Dict[str, List[State]] = defaultdict(list) + to_call: dict[str, list[State]] = defaultdict(list) for state in states: to_call[state.domain].append(state) - async def worker(domain: str, states_by_domain: List[State]) -> None: + async def worker(domain: str, states_by_domain: list[State]) -> None: try: integration = await async_get_integration(hass, domain) except IntegrationNotFound: @@ -92,7 +101,7 @@ async def async_reproduce_state( return try: - platform: Optional[ModuleType] = integration.get_platform("reproduce_state") + platform: ModuleType | None = integration.get_platform("reproduce_state") except ImportError: _LOGGER.warning("Integration %s does not support reproduce state", domain) return diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 2bc13fbdf44..5a08a97a210 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -1,9 +1,12 @@ """Helper to help store data.""" +from __future__ import annotations + import asyncio +from contextlib import suppress from json import JSONEncoder import logging import os -from typing import Any, Callable, Dict, List, Optional, Type, Union +from typing import Any, Callable from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback @@ -71,18 +74,18 @@ class Store: key: str, private: bool = False, *, - encoder: Optional[Type[JSONEncoder]] = None, + encoder: type[JSONEncoder] | None = None, ): """Initialize storage class.""" self.version = version self.key = key self.hass = hass self._private = private - self._data: Optional[Dict[str, Any]] = None - self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None - self._unsub_final_write_listener: Optional[CALLBACK_TYPE] = None + self._data: dict[str, Any] | None = None + self._unsub_delay_listener: CALLBACK_TYPE | None = None + self._unsub_final_write_listener: CALLBACK_TYPE | None = None self._write_lock = asyncio.Lock() - self._load_task: Optional[asyncio.Future] = None + self._load_task: asyncio.Future | None = None self._encoder = encoder @property @@ -90,7 +93,7 @@ class Store: """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) - async def async_load(self) -> Union[Dict, List, None]: + async def async_load(self) -> dict | list | None: """Load data. If the expected version does not match the given version, the migrate @@ -140,7 +143,7 @@ class Store: return stored - async def async_save(self, data: Union[Dict, List]) -> None: + async def async_save(self, data: dict | list) -> None: """Save data.""" self._data = {"version": self.version, "key": self.key, "data": data} @@ -151,7 +154,7 @@ class Store: await self._async_handle_write_data() @callback - def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> None: + def async_delay_save(self, data_func: Callable[[], dict], delay: float = 0) -> None: """Save data with an optional delay.""" self._data = {"version": self.version, "key": self.key, "data_func": data_func} @@ -224,7 +227,7 @@ class Store: except (json_util.SerializationError, json_util.WriteError) as err: _LOGGER.error("Error writing config for %s: %s", self.key, err) - def _write_data(self, path: str, data: Dict) -> None: + def _write_data(self, path: str, data: dict) -> None: """Write the data.""" if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) @@ -241,7 +244,5 @@ class Store: self._async_cleanup_delay_listener() self._async_cleanup_final_write_listener() - try: + with suppress(FileNotFoundError): await self.hass.async_add_executor_job(os.unlink, self.path) - except FileNotFoundError: - pass diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 2b82e19b8ce..b3a37d238f9 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -2,24 +2,22 @@ from __future__ import annotations import datetime -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util -from .typing import HomeAssistantType - if TYPE_CHECKING: - import astral # pylint: disable=unused-import + import astral DATA_LOCATION_CACHE = "astral_location_cache" @callback @bind_hass -def get_astral_location(hass: HomeAssistantType) -> astral.Location: +def get_astral_location(hass: HomeAssistant) -> astral.Location: """Get an astral location for the current Home Assistant configuration.""" from astral import Location # pylint: disable=import-outside-toplevel @@ -42,10 +40,10 @@ def get_astral_location(hass: HomeAssistantType) -> astral.Location: @callback @bind_hass def get_astral_event_next( - hass: HomeAssistantType, + hass: HomeAssistant, event: str, - utc_point_in_time: Optional[datetime.datetime] = None, - offset: Optional[datetime.timedelta] = None, + utc_point_in_time: datetime.datetime | None = None, + offset: datetime.timedelta | None = None, ) -> datetime.datetime: """Calculate the next specified solar event.""" location = get_astral_location(hass) @@ -54,10 +52,10 @@ def get_astral_event_next( @callback def get_location_astral_event_next( - location: "astral.Location", + location: astral.Location, event: str, - utc_point_in_time: Optional[datetime.datetime] = None, - offset: Optional[datetime.timedelta] = None, + utc_point_in_time: datetime.datetime | None = None, + offset: datetime.timedelta | None = None, ) -> datetime.datetime: """Calculate the next specified solar event.""" from astral import AstralError # pylint: disable=import-outside-toplevel @@ -89,10 +87,10 @@ def get_location_astral_event_next( @callback @bind_hass def get_astral_event_date( - hass: HomeAssistantType, + hass: HomeAssistant, event: str, - date: Union[datetime.date, datetime.datetime, None] = None, -) -> Optional[datetime.datetime]: + date: datetime.date | datetime.datetime | None = None, +) -> datetime.datetime | None: """Calculate the astral event time for the specified date.""" from astral import AstralError # pylint: disable=import-outside-toplevel @@ -114,7 +112,7 @@ def get_astral_event_date( @callback @bind_hass def is_up( - hass: HomeAssistantType, utc_point_in_time: Optional[datetime.datetime] = None + hass: HomeAssistant, utc_point_in_time: datetime.datetime | None = None ) -> bool: """Calculate if the sun is currently up.""" if utc_point_in_time is None: diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 9c2c4b181ad..6d6c912f8c9 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -1,17 +1,18 @@ """Helper to gather system info.""" +from __future__ import annotations + import os import platform -from typing import Any, Dict +from typing import Any from homeassistant.const import __version__ as current_version +from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass from homeassistant.util.package import is_virtual_env -from .typing import HomeAssistantType - @bind_hass -async def async_get_system_info(hass: HomeAssistantType) -> Dict[str, Any]: +async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: """Return info about the system.""" info_object = { "installation_type": "Unknown", diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 18ca8355159..e0f089e93b9 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -1,6 +1,7 @@ """Temperature helpers for Home Assistant.""" +from __future__ import annotations + from numbers import Number -from typing import Optional from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant @@ -8,8 +9,8 @@ from homeassistant.util.temperature import convert as convert_temperature def display_temp( - hass: HomeAssistant, temperature: Optional[float], unit: str, precision: float -) -> Optional[float]: + hass: HomeAssistant, temperature: float | None, unit: str, precision: float +) -> float | None: """Convert temperature into preferred units/precision for display.""" temperature_unit = unit ha_unit = hass.config.units.temperature_unit diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7377120af40..9580da82d65 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -5,6 +5,8 @@ from ast import literal_eval import asyncio import base64 import collections.abc +from contextlib import suppress +from contextvars import ContextVar from datetime import datetime, timedelta from functools import partial, wraps import json @@ -13,7 +15,7 @@ import math from operator import attrgetter import random import re -from typing import Any, Dict, Generator, Iterable, Optional, Type, Union, cast +from typing import Any, Generator, Iterable, cast from urllib.parse import urlencode as urllib_urlencode import weakref @@ -31,10 +33,16 @@ from homeassistant.const import ( LENGTH_METERS, STATE_UNKNOWN, ) -from homeassistant.core import State, callback, split_entity_id, valid_entity_id +from homeassistant.core import ( + HomeAssistant, + State, + callback, + split_entity_id, + valid_entity_id, +) from homeassistant.exceptions import TemplateError from homeassistant.helpers import entity_registry, location as loc_helper -from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe @@ -72,9 +80,11 @@ _COLLECTABLE_STATE_ATTRIBUTES = { ALL_STATES_RATE_LIMIT = timedelta(minutes=1) DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) +template_cv: ContextVar[str | None] = ContextVar("template_cv", default=None) + @bind_hass -def attach(hass: HomeAssistantType, obj: Any) -> None: +def attach(hass: HomeAssistant, obj: Any) -> None: """Recursively attach hass to all template instances in list and dict.""" if isinstance(obj, list): for child in obj: @@ -125,7 +135,7 @@ def is_template_string(maybe_template: str) -> bool: class ResultWrapper: """Result wrapper class to store render result.""" - render_result: Optional[str] + render_result: str | None def gen_result_wrapper(kls): @@ -134,7 +144,7 @@ def gen_result_wrapper(kls): class Wrapper(kls, ResultWrapper): """Wrapper of a kls that can store render_result.""" - def __init__(self, *args: tuple, render_result: Optional[str] = None) -> None: + def __init__(self, *args: tuple, render_result: str | None = None) -> None: super().__init__(*args) self.render_result = render_result @@ -156,15 +166,13 @@ class TupleWrapper(tuple, ResultWrapper): # This is all magic to be allowed to subclass a tuple. - def __new__( - cls, value: tuple, *, render_result: Optional[str] = None - ) -> TupleWrapper: + def __new__(cls, value: tuple, *, render_result: str | None = None) -> TupleWrapper: """Create a new tuple class.""" return super().__new__(cls, tuple(value)) # pylint: disable=super-init-not-called - def __init__(self, value: tuple, *, render_result: Optional[str] = None): + def __init__(self, value: tuple, *, render_result: str | None = None): """Initialize a new tuple class.""" self.render_result = render_result @@ -176,7 +184,7 @@ class TupleWrapper(tuple, ResultWrapper): return self.render_result -RESULT_WRAPPERS: Dict[Type, Type] = { +RESULT_WRAPPERS: dict[type, type] = { kls: gen_result_wrapper(kls) # type: ignore[no-untyped-call] for kls in (list, dict, set) } @@ -200,15 +208,15 @@ class RenderInfo: # Will be set sensibly once frozen. self.filter_lifecycle = _true self.filter = _true - self._result: Optional[str] = None + self._result: str | None = None self.is_static = False - self.exception: Optional[TemplateError] = None + self.exception: TemplateError | None = None self.all_states = False self.all_states_lifecycle = False self.domains = set() self.domains_lifecycle = set() self.entities = set() - self.rate_limit: Optional[timedelta] = None + self.rate_limit: timedelta | None = None self.has_time = False def __repr__(self) -> str: @@ -294,7 +302,7 @@ class Template: self.template: str = template.strip() self._compiled_code = None - self._compiled: Optional[Template] = None + self._compiled: jinja2.Template | None = None self.hass = hass self.is_static = not is_template_string(template) self._limited = None @@ -304,7 +312,7 @@ class Template: if self.hass is None: return _NO_HASS_ENV wanted_env = _ENVIRONMENT_LIMITED if self._limited else _ENVIRONMENT - ret: Optional[TemplateEnvironment] = self.hass.data.get(wanted_env) + ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited) # type: ignore[no-untyped-call] return ret @@ -331,7 +339,7 @@ class Template: If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) @@ -355,7 +363,7 @@ class Template: If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. """ if self.is_static: - if self.hass.config.legacy_templates or not parse_result: + if not parse_result or self.hass.config.legacy_templates: return self.template return self._parse_result(self.template) @@ -365,8 +373,8 @@ class Template: kwargs.update(variables) try: - render_result = compiled.render(kwargs) - except Exception as err: # pylint: disable=broad-except + render_result = _render_with_context(self.template, compiled, **kwargs) + except Exception as err: raise TemplateError(err) from err render_result = render_result.strip() @@ -425,8 +433,6 @@ class Template: This method must be run in the event loop. """ - assert self.hass - if self.is_static: return False @@ -439,7 +445,7 @@ class Template: def _render_template() -> None: try: - compiled.render(kwargs) + _render_with_context(self.template, compiled, **kwargs) except TimeoutError: pass finally: @@ -517,13 +523,13 @@ class Template: variables = dict(variables or {}) variables["value"] = value - try: + with suppress(ValueError, TypeError): variables["value_json"] = json.loads(value) - except (ValueError, TypeError): - pass try: - return self._compiled.render(variables).strip() + return _render_with_context( + self.template, self._compiled, **variables + ).strip() except jinja2.TemplateError as ex: if error_value is _SENTINEL: _LOGGER.error( @@ -534,7 +540,7 @@ class Template: ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self, limited: bool = False) -> Template: + def _ensure_compiled(self, limited: bool = False) -> jinja2.Template: """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -547,7 +553,7 @@ class Template: env = self._env self._compiled = cast( - Template, + jinja2.Template, jinja2.Template.from_code(env, self._compiled_code, env.globals, None), ) @@ -573,7 +579,7 @@ class Template: class AllStates: """Class to expose all HA states as attributes.""" - def __init__(self, hass: HomeAssistantType) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize all states.""" self._hass = hass @@ -627,7 +633,7 @@ class AllStates: class DomainStates: """Class to expose a specific HA domain as attributes.""" - def __init__(self, hass: HomeAssistantType, domain: str) -> None: + def __init__(self, hass: HomeAssistant, domain: str) -> None: """Initialize the domain states.""" self._hass = hass self._domain = domain @@ -672,9 +678,7 @@ class TemplateState(State): # Inheritance is done so functions that check against State keep working # pylint: disable=super-init-not-called - def __init__( - self, hass: HomeAssistantType, state: State, collect: bool = True - ) -> None: + def __init__(self, hass: HomeAssistant, state: State, collect: bool = True) -> None: """Initialize template state.""" self._hass = hass self._state = state @@ -772,34 +776,32 @@ class TemplateState(State): return f"