Merge pull request #48782 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-04-07 19:07:10 +02:00 committed by GitHub
commit 8bdcdfb8e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4716 changed files with 92619 additions and 31942 deletions

View File

@ -221,6 +221,7 @@ omit =
homeassistant/components/ecobee/weather.py homeassistant/components/ecobee/weather.py
homeassistant/components/econet/__init__.py homeassistant/components/econet/__init__.py
homeassistant/components/econet/binary_sensor.py homeassistant/components/econet/binary_sensor.py
homeassistant/components/econet/climate.py
homeassistant/components/econet/const.py homeassistant/components/econet/const.py
homeassistant/components/econet/sensor.py homeassistant/components/econet/sensor.py
homeassistant/components/econet/water_heater.py homeassistant/components/econet/water_heater.py
@ -314,7 +315,6 @@ omit =
homeassistant/components/foscam/camera.py homeassistant/components/foscam/camera.py
homeassistant/components/foursquare/* homeassistant/components/foursquare/*
homeassistant/components/free_mobile/notify.py homeassistant/components/free_mobile/notify.py
homeassistant/components/freebox/__init__.py
homeassistant/components/freebox/device_tracker.py homeassistant/components/freebox/device_tracker.py
homeassistant/components/freebox/router.py homeassistant/components/freebox/router.py
homeassistant/components/freebox/sensor.py homeassistant/components/freebox/sensor.py
@ -377,6 +377,9 @@ omit =
homeassistant/components/harmony/data.py homeassistant/components/harmony/data.py
homeassistant/components/harmony/remote.py homeassistant/components/harmony/remote.py
homeassistant/components/harmony/util.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/haveibeenpwned/sensor.py
homeassistant/components/hdmi_cec/* homeassistant/components/hdmi_cec/*
homeassistant/components/heatmiser/climate.py homeassistant/components/heatmiser/climate.py
@ -384,7 +387,13 @@ omit =
homeassistant/components/hikvisioncam/switch.py homeassistant/components/hikvisioncam/switch.py
homeassistant/components/hisense_aehw4a1/* homeassistant/components/hisense_aehw4a1/*
homeassistant/components/hitron_coda/device_tracker.py 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/__init__.py
homeassistant/components/hlk_sw16/switch.py homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/* homeassistant/components/home_connect/*
@ -392,6 +401,9 @@ omit =
homeassistant/components/homematic/climate.py homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py homeassistant/components/homematic/cover.py
homeassistant/components/homematic/notify.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/homeworks/*
homeassistant/components/honeywell/climate.py homeassistant/components/honeywell/climate.py
homeassistant/components/horizon/media_player.py homeassistant/components/horizon/media_player.py
@ -497,7 +509,18 @@ omit =
homeassistant/components/lastfm/sensor.py homeassistant/components/lastfm/sensor.py
homeassistant/components/launch_library/const.py homeassistant/components/launch_library/const.py
homeassistant/components/launch_library/sensor.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_netcast/media_player.py
homeassistant/components/lg_soundbar/media_player.py homeassistant/components/lg_soundbar/media_player.py
homeassistant/components/life360/* homeassistant/components/life360/*
@ -540,7 +563,6 @@ omit =
homeassistant/components/map/* homeassistant/components/map/*
homeassistant/components/mastodon/notify.py homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/* homeassistant/components/matrix/*
homeassistant/components/maxcube/*
homeassistant/components/mcp23017/* homeassistant/components/mcp23017/*
homeassistant/components/media_extractor/* homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py homeassistant/components/mediaroom/media_player.py
@ -624,17 +646,6 @@ omit =
homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nello/lock.py homeassistant/components/nello/lock.py
homeassistant/components/nest/legacy/* 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/netdata/sensor.py
homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear/device_tracker.py
homeassistant/components/netgear_lte/* homeassistant/components/netgear_lte/*
@ -723,12 +734,14 @@ omit =
homeassistant/components/pencom/switch.py homeassistant/components/pencom/switch.py
homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/media_player.py
homeassistant/components/philips_js/remote.py
homeassistant/components/pi_hole/sensor.py homeassistant/components/pi_hole/sensor.py
homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
homeassistant/components/pi4ioe5v9xxxx/switch.py homeassistant/components/pi4ioe5v9xxxx/switch.py
homeassistant/components/picotts/tts.py homeassistant/components/picotts/tts.py
homeassistant/components/piglow/light.py homeassistant/components/piglow/light.py
homeassistant/components/pilight/* homeassistant/components/pilight/*
homeassistant/components/ping/__init__.py
homeassistant/components/ping/const.py homeassistant/components/ping/const.py
homeassistant/components/ping/binary_sensor.py homeassistant/components/ping/binary_sensor.py
homeassistant/components/ping/device_tracker.py homeassistant/components/ping/device_tracker.py
@ -782,6 +795,7 @@ omit =
homeassistant/components/raspyrfm/* homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/__init__.py
homeassistant/components/recollect_waste/sensor.py homeassistant/components/recollect_waste/sensor.py
homeassistant/components/recorder/repack.py
homeassistant/components/recswitch/switch.py homeassistant/components/recswitch/switch.py
homeassistant/components/reddit/* homeassistant/components/reddit/*
homeassistant/components/rejseplanen/sensor.py homeassistant/components/rejseplanen/sensor.py
@ -823,6 +837,11 @@ omit =
homeassistant/components/satel_integra/* homeassistant/components/satel_integra/*
homeassistant/components/schluter/* homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py 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/*
homeassistant/components/scsgate/cover.py homeassistant/components/scsgate/cover.py
homeassistant/components/sendgrid/notify.py homeassistant/components/sendgrid/notify.py
@ -1045,7 +1064,14 @@ omit =
homeassistant/components/velbus/switch.py homeassistant/components/velbus/switch.py
homeassistant/components/velux/* homeassistant/components/velux/*
homeassistant/components/venstar/climate.py 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/versasense/*
homeassistant/components/vesync/__init__.py homeassistant/components/vesync/__init__.py
homeassistant/components/vesync/common.py homeassistant/components/vesync/common.py
@ -1164,3 +1190,6 @@ exclude_lines =
# Don't complain if tests don't hit defensive assertion code: # Don't complain if tests don't hit defensive assertion code:
raise AssertionError raise AssertionError
raise NotImplementedError raise NotImplementedError
# TYPE_CHECKING block is never executed during pytest run
if TYPE_CHECKING:

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
custom: https://www.nabucasa.com
github: balloob

View File

@ -1,5 +1,5 @@
name: Report an issue with Home Assistant Core 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: "" title: ""
issue_body: true issue_body: true
body: body:
@ -26,6 +26,7 @@ body:
value: | value: |
## Environment ## Environment
- type: input - type: input
id: version
validations: validations:
required: true required: true
attributes: attributes:
@ -52,11 +53,13 @@ body:
- Home Assistant Supervised - Home Assistant Supervised
- Home Assistant Core - Home Assistant Core
- type: input - type: input
id: integration_name
attributes: attributes:
label: Integration causing the issue label: Integration causing the issue
description: > description: >
The name of the integration, for example, Automation or Philips Hue. The name of the integration, for example, Automation or Philips Hue.
- type: input - type: input
id: integration_link
attributes: attributes:
label: Link to integration documentation on our website label: Link to integration documentation on our website
placeholder: "https://www.home-assistant.io/integrations/..." placeholder: "https://www.home-assistant.io/integrations/..."
@ -76,20 +79,12 @@ body:
description: | description: |
If this issue has an example piece of YAML that can help reproducing this problem, please provide. 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. This can be an piece of YAML from, e.g., an automation, script, scene or configuration.
value: | render: yaml
```yaml
# Put your YAML below this line
```
- type: textarea - type: textarea
attributes: attributes:
label: Anything in the logs that might be useful for us? label: Anything in the logs that might be useful for us?
description: For example, error message, or stack traces. description: For example, error message, or stack traces.
value: | render: txt
```txt
# Put your logs below this line
```
- type: markdown - type: markdown
attributes: attributes:
value: | value: |

View File

@ -12,7 +12,7 @@ on:
env: env:
CACHE_VERSION: 1 CACHE_VERSION: 1
DEFAULT_PYTHON: 3.8 DEFAULT_PYTHON: 3.8
PRE_COMMIT_HOME: ~/.cache/pre-commit PRE_COMMIT_CACHE: ~/.cache/pre-commit
jobs: jobs:
# Separate job to pre-populate the base dependency cache # Separate job to pre-populate the base dependency cache
@ -20,6 +20,9 @@ jobs:
prepare-base: prepare-base:
name: Prepare base dependencies name: Prepare base dependencies
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs:
python-key: ${{ steps.generate-python-key.outputs.key }}
pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -28,21 +31,25 @@ jobs:
uses: actions/setup-python@v2.2.1 uses: actions/setup-python@v2.2.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} 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 - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: >-
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.python.outputs.python-version }}-${{ steps.generate-python-key.outputs.key }}
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
restore-keys: | restore-keys: |
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_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 }}-
- name: Create Python virtual environment - name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -50,15 +57,20 @@ jobs:
. venv/bin/activate . venv/bin/activate
pip install -U "pip<20.3" setuptools pip install -U "pip<20.3" setuptools
pip install -r requirements.txt -r requirements_test.txt 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 - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: >-
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }}
restore-keys: | restore-keys: |
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}-
- name: Install pre-commit dependencies - name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true' if: steps.cache-precommit.outputs.cache-hit != 'true'
run: | run: |
@ -82,12 +94,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -97,13 +105,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Run bandit - name: Run bandit
run: | run: |
@ -127,12 +134,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -142,13 +145,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Run black - name: Run black
run: | run: |
@ -172,12 +174,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -187,13 +185,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Register codespell problem matcher - name: Register codespell problem matcher
run: | run: |
@ -239,12 +236,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -254,13 +247,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Register check executables problem matcher - name: Register check executables problem matcher
run: | run: |
@ -287,12 +279,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -302,13 +290,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Register flake8 problem matcher - name: Register flake8 problem matcher
run: | run: |
@ -335,12 +322,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -350,13 +333,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Run isort - name: Run isort
run: | run: |
@ -380,12 +362,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -395,13 +373,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Register check-json problem matcher - name: Register check-json problem matcher
run: | run: |
@ -428,12 +405,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -443,13 +416,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Run pyupgrade - name: Run pyupgrade
run: | run: |
@ -484,12 +456,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -499,13 +467,12 @@ jobs:
id: cache-precommit id: cache-precommit
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if pre-commit cache restore failed
- name: Fail job if cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore pre-commit environment from cache"
exit 1 exit 1
- name: Register yamllint problem matcher - name: Register yamllint problem matcher
run: | run: |
@ -531,11 +498,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ matrix.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ needs.prepare-tests.outputs.python-key }}
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -563,12 +527,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ needs.prepare-base.outputs.python-key }}
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -585,24 +545,31 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: [3.8, 3.9] python-version: [3.8, 3.9]
outputs:
python-key: ${{ steps.generate-python-key.outputs.key }}
container: homeassistant/ci-azure:${{ matrix.python-version }} container: homeassistant/ci-azure:${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 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 - name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: >-
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ ${{ runner.os }}-${{ matrix.python-version }}-${{
matrix.python-version }}-${{ hashFiles('requirements_test.txt') steps.generate-python-key.outputs.key }}
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
restore-keys: | restore-keys: |
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
- name: Create full Python ${{ matrix.python-version }} virtual environment - name: Create full Python ${{ matrix.python-version }} virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -633,11 +600,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ matrix.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ needs.prepare-tests.outputs.python-key }}
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -667,11 +631,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ matrix.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ needs.prepare-tests.outputs.python-key }}
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -689,6 +650,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: prepare-tests needs: prepare-tests
strategy: strategy:
fail-fast: false
matrix: matrix:
group: [1, 2, 3, 4] group: [1, 2, 3, 4]
python-version: [3.8, 3.9] python-version: [3.8, 3.9]
@ -703,11 +665,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ matrix.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ needs.prepare-tests.outputs.python-key }}
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -735,6 +694,7 @@ jobs:
--test-group-count 4 \ --test-group-count 4 \
--test-group=${{ matrix.group }} \ --test-group=${{ matrix.group }} \
--cov homeassistant \ --cov homeassistant \
--cov-report= \
-o console_output_style=count \ -o console_output_style=count \
-p no:sugar \ -p no:sugar \
tests tests
@ -750,7 +710,7 @@ jobs:
coverage: coverage:
name: Process test coverage name: Process test coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pytest needs: ["prepare-tests", "pytest"]
strategy: strategy:
matrix: matrix:
python-version: [3.8] python-version: [3.8]
@ -763,11 +723,8 @@ jobs:
uses: actions/cache@v2.1.4 uses: actions/cache@v2.1.4
with: with:
path: venv path: venv
key: >- key: ${{ runner.os }}-${{ matrix.python-version }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ needs.prepare-tests.outputs.python-key }}
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@ -782,4 +739,4 @@ jobs:
coverage report --fail-under=94 coverage report --fail-under=94
coverage xml coverage xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.2.1 uses: codecov/codecov-action@v1.3.1

View File

@ -16,7 +16,7 @@ jobs:
# - No PRs marked as no-stale # - No PRs marked as no-stale
# - No issues marked as no-stale or help-wanted # - No issues marked as no-stale or help-wanted
- name: 90 days stale issues & PRs policy - name: 90 days stale issues & PRs policy
uses: actions/stale@v3.0.17 uses: actions/stale@v3.0.18
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90
@ -53,7 +53,7 @@ jobs:
# - No PRs marked as no-stale or new-integrations # - No PRs marked as no-stale or new-integrations
# - No issues (-1) # - No issues (-1)
- name: 30 days stale PRs policy - name: 30 days stale PRs policy
uses: actions/stale@v3.0.17 uses: actions/stale@v3.0.18
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30 days-before-stale: 30
@ -78,7 +78,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted # - No Issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: Needs more information stale issues policy - name: Needs more information stale issues policy
uses: actions/stale@v3.0.17 uses: actions/stale@v3.0.18
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "needs-more-information" only-labels: "needs-more-information"

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.7.2 rev: v2.11.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
@ -23,12 +23,16 @@ repos:
exclude_types: [csv, json] exclude_types: [csv, json]
exclude: ^tests/fixtures/ exclude: ^tests/fixtures/
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4 rev: 3.9.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
- flake8-docstrings==1.5.0 - pycodestyle==2.7.0
- pydocstyle==5.1.1 - 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$ files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.0 rev: 1.7.0
@ -40,7 +44,7 @@ repos:
- --configfile=tests/bandit.yaml - --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$ files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.5.3 rev: 5.7.0
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
@ -64,6 +68,19 @@ repos:
hooks: hooks:
- id: prettier - id: prettier
stages: [manual] 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 - repo: local
hooks: hooks:
# Run mypy through our wrapper script in order to get the possible # Run mypy through our wrapper script in order to get the possible

View File

@ -36,6 +36,7 @@ homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya homeassistant/components/ambient_station/* @bachya
homeassistant/components/amcrest/* @pnbruckner homeassistant/components/amcrest/* @pnbruckner
homeassistant/components/analytics/* @home-assistant/core @ludeeus
homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/androidtv/* @JeffLIrion
homeassistant/components/apache_kafka/* @bachya homeassistant/components/apache_kafka/* @bachya
homeassistant/components/api/* @home-assistant/core homeassistant/components/api/* @home-assistant/core
@ -180,14 +181,12 @@ homeassistant/components/google_cloud/* @lufton
homeassistant/components/gpsd/* @fabaff homeassistant/components/gpsd/* @fabaff
homeassistant/components/gree/* @cmroche homeassistant/components/gree/* @cmroche
homeassistant/components/greeneye_monitor/* @jkeljo homeassistant/components/greeneye_monitor/* @jkeljo
homeassistant/components/griddy/* @bdraco
homeassistant/components/group/* @home-assistant/core homeassistant/components/group/* @home-assistant/core
homeassistant/components/growatt_server/* @indykoning homeassistant/components/growatt_server/* @indykoning
homeassistant/components/guardian/* @bachya homeassistant/components/guardian/* @bachya
homeassistant/components/habitica/* @ASMfreaK @leikoilja homeassistant/components/habitica/* @ASMfreaK @leikoilja
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
homeassistant/components/hassio/* @home-assistant/supervisor homeassistant/components/hassio/* @home-assistant/supervisor
homeassistant/components/hdmi_cec/* @newAM
homeassistant/components/heatmiser/* @andylockran homeassistant/components/heatmiser/* @andylockran
homeassistant/components/heos/* @andrewsayre homeassistant/components/heos/* @andrewsayre
homeassistant/components/here_travel_time/* @eifinger homeassistant/components/here_travel_time/* @eifinger
@ -198,11 +197,11 @@ homeassistant/components/history/* @home-assistant/core
homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/hlk_sw16/* @jameshilliard homeassistant/components/hlk_sw16/* @jameshilliard
homeassistant/components/home_connect/* @DavidMStraub homeassistant/components/home_connect/* @DavidMStraub
homeassistant/components/home_plus_control/* @chemaaa
homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homekit_controller/* @Jc2k
homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/homematicip_cloud/* @SukramJ
homeassistant/components/http/* @home-assistant/core homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/huawei_router/* @abmantis homeassistant/components/huawei_router/* @abmantis
@ -403,6 +402,7 @@ homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps homeassistant/components/schluter/* @prairieapps
homeassistant/components/scrape/* @fabaff homeassistant/components/scrape/* @fabaff
homeassistant/components/screenlogic/* @dieselrabbit
homeassistant/components/script/* @home-assistant/core homeassistant/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core homeassistant/components/search/* @home-assistant/core
homeassistant/components/sense/* @kbickar homeassistant/components/sense/* @kbickar
@ -433,12 +433,12 @@ homeassistant/components/smarttub/* @mdz
homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smarty/* @z0mbieprocess
homeassistant/components/sms/* @ocalvo homeassistant/components/sms/* @ocalvo
homeassistant/components/smtp/* @fabaff homeassistant/components/smtp/* @fabaff
homeassistant/components/solaredge/* @frenck
homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solaredge_local/* @drobtravels @scheric
homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solarlog/* @Ernst79
homeassistant/components/solax/* @squishykid homeassistant/components/solax/* @squishykid
homeassistant/components/soma/* @ratsept homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne homeassistant/components/somfy/* @tetienne
homeassistant/components/somfy_mylink/* @bdraco
homeassistant/components/sonarr/* @ctalkington homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @cgtobi homeassistant/components/sonos/* @cgtobi
@ -454,7 +454,7 @@ homeassistant/components/starline/* @anonym-tsk
homeassistant/components/statistics/* @fabaff homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stiebel_eltron/* @fucm
homeassistant/components/stookalert/* @fwestenberg homeassistant/components/stookalert/* @fwestenberg
homeassistant/components/stream/* @hunterjm @uvjustin homeassistant/components/stream/* @hunterjm @uvjustin @allenporter
homeassistant/components/stt/* @pvizeli homeassistant/components/stt/* @pvizeli
homeassistant/components/subaru/* @G-Two homeassistant/components/subaru/* @G-Two
homeassistant/components/suez_water/* @ooii homeassistant/components/suez_water/* @ooii
@ -492,6 +492,7 @@ homeassistant/components/toon/* @frenck
homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/tplink/* @rytilahti @thegardenmonkey
homeassistant/components/traccar/* @ludeeus homeassistant/components/traccar/* @ludeeus
homeassistant/components/trace/* @home-assistant/core
homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force
homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/transmission/* @engrbm87 @JPHutchins
@ -512,7 +513,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte
homeassistant/components/utility_meter/* @dgomes homeassistant/components/utility_meter/* @dgomes
homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velbus/* @Cereal2nd @brefra
homeassistant/components/velux/* @Julius2342 homeassistant/components/velux/* @Julius2342
homeassistant/components/vera/* @vangorra homeassistant/components/vera/* @pavoni
homeassistant/components/verisure/* @frenck homeassistant/components/verisure/* @frenck
homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/versasense/* @flamm3blemuff1n
homeassistant/components/version/* @fabaff @ludeeus homeassistant/components/version/* @fabaff @ludeeus
@ -524,6 +525,7 @@ homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf @dmcc homeassistant/components/vlc_telnet/* @rodripf @dmcc
homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volkszaehler/* @fabaff
homeassistant/components/volumio/* @OnFreund homeassistant/components/volumio/* @OnFreund
homeassistant/components/wake_on_lan/* @ntilley905
homeassistant/components/waqi/* @andrey-git homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff homeassistant/components/weather/* @fabaff

View File

@ -28,10 +28,11 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
WORKDIR /workspaces WORKDIR /workspaces
# Install Python dependencies from requirements # 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 COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
RUN pip3 install -r requirements_test.txt \ RUN pip3 install -r requirements.txt \
&& rm -rf requirements_test.txt requirements_test_pre_commit.txt homeassistant/ && 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 # Set the default shell to bash instead of sh
ENV SHELL /bin/bash ENV SHELL /bin/bash

View File

@ -1,11 +1,12 @@
"""Start Home Assistant.""" """Start Home Assistant."""
from __future__ import annotations
import argparse import argparse
import os import os
import platform import platform
import subprocess import subprocess
import sys import sys
import threading import threading
from typing import List
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ 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 pass
def cmdline() -> List[str]: def cmdline() -> list[str]:
"""Collect path and arguments to re-execute the current hass instance.""" """Collect path and arguments to re-execute the current hass instance."""
if os.path.basename(sys.argv[0]) == "__main__.py": if os.path.basename(sys.argv[0]) == "__main__.py":
modulepath = os.path.dirname(sys.argv[0]) modulepath = os.path.dirname(sys.argv[0])

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple, cast from typing import Any, Dict, Optional, Tuple, cast
import jwt import jwt
@ -36,8 +36,8 @@ class InvalidProvider(Exception):
async def auth_manager_from_config( async def auth_manager_from_config(
hass: HomeAssistant, hass: HomeAssistant,
provider_configs: List[Dict[str, Any]], provider_configs: list[dict[str, Any]],
module_configs: List[Dict[str, Any]], module_configs: list[dict[str, Any]],
) -> AuthManager: ) -> AuthManager:
"""Initialize an auth manager from config. """Initialize an auth manager from config.
@ -87,8 +87,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
self, self,
handler_key: Any, handler_key: Any,
*, *,
context: Optional[Dict[str, Any]] = None, context: dict[str, Any] | None = None,
data: Optional[Dict[str, Any]] = None, data: dict[str, Any] | None = None,
) -> data_entry_flow.FlowHandler: ) -> data_entry_flow.FlowHandler:
"""Create a login flow.""" """Create a login flow."""
auth_provider = self.auth_manager.get_auth_provider(*handler_key) 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) return await auth_provider.async_login_flow(context)
async def async_finish_flow( async def async_finish_flow(
self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] self, flow: data_entry_flow.FlowHandler, result: dict[str, Any]
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Return a user as result of login flow.""" """Return a user as result of login flow."""
flow = cast(LoginFlow, flow) flow = cast(LoginFlow, flow)
@ -157,22 +157,22 @@ class AuthManager:
self.login_flow = AuthManagerFlowManager(hass, self) self.login_flow = AuthManagerFlowManager(hass, self)
@property @property
def auth_providers(self) -> List[AuthProvider]: def auth_providers(self) -> list[AuthProvider]:
"""Return a list of available auth providers.""" """Return a list of available auth providers."""
return list(self._providers.values()) return list(self._providers.values())
@property @property
def auth_mfa_modules(self) -> List[MultiFactorAuthModule]: def auth_mfa_modules(self) -> list[MultiFactorAuthModule]:
"""Return a list of available auth modules.""" """Return a list of available auth modules."""
return list(self._mfa_modules.values()) return list(self._mfa_modules.values())
def get_auth_provider( def get_auth_provider(
self, provider_type: str, provider_id: Optional[str] self, provider_type: str, provider_id: str | None
) -> Optional[AuthProvider]: ) -> AuthProvider | None:
"""Return an auth provider, None if not found.""" """Return an auth provider, None if not found."""
return self._providers.get((provider_type, provider_id)) 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 a List of auth provider of one type, Empty if not found."""
return [ return [
provider provider
@ -180,30 +180,30 @@ class AuthManager:
if p_type == provider_type 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 a multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id) 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.""" """Retrieve all users."""
return await self._store.async_get_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.""" """Retrieve a user."""
return await self._store.async_get_user(user_id) 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.""" """Retrieve the owner."""
users = await self.async_get_users() users = await self.async_get_users()
return next((user for user in users if user.is_owner), None) 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.""" """Retrieve all groups."""
return await self._store.async_get_group(group_id) return await self._store.async_get_group(group_id)
async def async_get_user_by_credentials( async def async_get_user_by_credentials(
self, credentials: models.Credentials self, credentials: models.Credentials
) -> Optional[models.User]: ) -> models.User | None:
"""Get a user by credential, return None if not found.""" """Get a user by credential, return None if not found."""
for user in await self.async_get_users(): for user in await self.async_get_users():
for creds in user.credentials: for creds in user.credentials:
@ -213,7 +213,7 @@ class AuthManager:
return None return None
async def async_create_system_user( 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: ) -> models.User:
"""Create a system user.""" """Create a system user."""
user = await self._store.async_create_user( user = await self._store.async_create_user(
@ -225,10 +225,10 @@ class AuthManager:
return user return user
async def async_create_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: ) -> models.User:
"""Create a user.""" """Create a user."""
kwargs: Dict[str, Any] = { kwargs: dict[str, Any] = {
"name": name, "name": name,
"is_active": True, "is_active": True,
"group_ids": group_ids or [], "group_ids": group_ids or [],
@ -294,12 +294,12 @@ class AuthManager:
async def async_update_user( async def async_update_user(
self, self,
user: models.User, user: models.User,
name: Optional[str] = None, name: str | None = None,
is_active: Optional[bool] = None, is_active: bool | None = None,
group_ids: Optional[List[str]] = None, group_ids: list[str] | None = None,
) -> None: ) -> None:
"""Update a user.""" """Update a user."""
kwargs: Dict[str, Any] = {} kwargs: dict[str, Any] = {}
if name is not None: if name is not None:
kwargs["name"] = name kwargs["name"] = name
if group_ids is not None: if group_ids is not None:
@ -362,9 +362,9 @@ class AuthManager:
await module.async_depose_user(user.id) 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.""" """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(): for module_id, module in self._mfa_modules.items():
if await module.async_is_user_setup(user.id): if await module.async_is_user_setup(user.id):
modules[module_id] = module.name modules[module_id] = module.name
@ -373,12 +373,12 @@ class AuthManager:
async def async_create_refresh_token( async def async_create_refresh_token(
self, self,
user: models.User, user: models.User,
client_id: Optional[str] = None, client_id: str | None = None,
client_name: Optional[str] = None, client_name: str | None = None,
client_icon: Optional[str] = None, client_icon: str | None = None,
token_type: Optional[str] = None, token_type: str | None = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
credential: Optional[models.Credentials] = None, credential: models.Credentials | None = None,
) -> models.RefreshToken: ) -> models.RefreshToken:
"""Create a new refresh token for a user.""" """Create a new refresh token for a user."""
if not user.is_active: if not user.is_active:
@ -432,13 +432,13 @@ class AuthManager:
async def async_get_refresh_token( async def async_get_refresh_token(
self, token_id: str self, token_id: str
) -> Optional[models.RefreshToken]: ) -> models.RefreshToken | None:
"""Get refresh token by id.""" """Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id) return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token( async def async_get_refresh_token_by_token(
self, token: str self, token: str
) -> Optional[models.RefreshToken]: ) -> models.RefreshToken | None:
"""Get refresh token by token.""" """Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token) return await self._store.async_get_refresh_token_by_token(token)
@ -450,7 +450,7 @@ class AuthManager:
@callback @callback
def async_create_access_token( 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: ) -> str:
"""Create a new access token.""" """Create a new access token."""
self.async_validate_refresh_token(refresh_token, remote_ip) self.async_validate_refresh_token(refresh_token, remote_ip)
@ -471,7 +471,7 @@ class AuthManager:
@callback @callback
def _async_resolve_provider( def _async_resolve_provider(
self, refresh_token: models.RefreshToken self, refresh_token: models.RefreshToken
) -> Optional[AuthProvider]: ) -> AuthProvider | None:
"""Get the auth provider for the given refresh token. """Get the auth provider for the given refresh token.
Raises an exception if the expected provider is no longer available or return Raises an exception if the expected provider is no longer available or return
@ -492,7 +492,7 @@ class AuthManager:
@callback @callback
def async_validate_refresh_token( 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: ) -> None:
"""Validate that a refresh token is usable. """Validate that a refresh token is usable.
@ -504,7 +504,7 @@ class AuthManager:
async def async_validate_access_token( async def async_validate_access_token(
self, token: str self, token: str
) -> Optional[models.RefreshToken]: ) -> models.RefreshToken | None:
"""Return refresh token if an access token is valid.""" """Return refresh token if an access token is valid."""
try: try:
unverif_claims = jwt.decode(token, verify=False) unverif_claims = jwt.decode(token, verify=False)
@ -535,7 +535,7 @@ class AuthManager:
@callback @callback
def _async_get_auth_provider( def _async_get_auth_provider(
self, credentials: models.Credentials self, credentials: models.Credentials
) -> Optional[AuthProvider]: ) -> AuthProvider | None:
"""Get auth provider from a set of credentials.""" """Get auth provider from a set of credentials."""
auth_provider_key = ( auth_provider_key = (
credentials.auth_provider_type, credentials.auth_provider_type,

View File

@ -1,10 +1,12 @@
"""Storage for auth models.""" """Storage for auth models."""
from __future__ import annotations
import asyncio import asyncio
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
import hmac import hmac
from logging import getLogger 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.auth.const import ACCESS_TOKEN_EXPIRATION
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -34,15 +36,15 @@ class AuthStore:
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the auth store.""" """Initialize the auth store."""
self.hass = hass self.hass = hass
self._users: Optional[Dict[str, models.User]] = None self._users: dict[str, models.User] | None = None
self._groups: Optional[Dict[str, models.Group]] = None self._groups: dict[str, models.Group] | None = None
self._perm_lookup: Optional[PermissionLookup] = None self._perm_lookup: PermissionLookup | None = None
self._store = hass.helpers.storage.Store( self._store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True STORAGE_VERSION, STORAGE_KEY, private=True
) )
self._lock = asyncio.Lock() 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.""" """Retrieve all users."""
if self._groups is None: if self._groups is None:
await self._async_load() await self._async_load()
@ -50,7 +52,7 @@ class AuthStore:
return list(self._groups.values()) 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.""" """Retrieve all users."""
if self._groups is None: if self._groups is None:
await self._async_load() await self._async_load()
@ -58,7 +60,7 @@ class AuthStore:
return self._groups.get(group_id) 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.""" """Retrieve all users."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -66,7 +68,7 @@ class AuthStore:
return list(self._users.values()) 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.""" """Retrieve a user by id."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -76,12 +78,12 @@ class AuthStore:
async def async_create_user( async def async_create_user(
self, self,
name: Optional[str], name: str | None,
is_owner: Optional[bool] = None, is_owner: bool | None = None,
is_active: Optional[bool] = None, is_active: bool | None = None,
system_generated: Optional[bool] = None, system_generated: bool | None = None,
credentials: Optional[models.Credentials] = None, credentials: models.Credentials | None = None,
group_ids: Optional[List[str]] = None, group_ids: list[str] | None = None,
) -> models.User: ) -> models.User:
"""Create a new user.""" """Create a new user."""
if self._users is None: if self._users is None:
@ -97,7 +99,7 @@ class AuthStore:
raise ValueError(f"Invalid group specified {group_id}") raise ValueError(f"Invalid group specified {group_id}")
groups.append(group) groups.append(group)
kwargs: Dict[str, Any] = { kwargs: dict[str, Any] = {
"name": name, "name": name,
# Until we get group management, we just put everyone in the # Until we get group management, we just put everyone in the
# same group. # same group.
@ -146,9 +148,9 @@ class AuthStore:
async def async_update_user( async def async_update_user(
self, self,
user: models.User, user: models.User,
name: Optional[str] = None, name: str | None = None,
is_active: Optional[bool] = None, is_active: bool | None = None,
group_ids: Optional[List[str]] = None, group_ids: list[str] | None = None,
) -> None: ) -> None:
"""Update a user.""" """Update a user."""
assert self._groups is not None assert self._groups is not None
@ -203,15 +205,15 @@ class AuthStore:
async def async_create_refresh_token( async def async_create_refresh_token(
self, self,
user: models.User, user: models.User,
client_id: Optional[str] = None, client_id: str | None = None,
client_name: Optional[str] = None, client_name: str | None = None,
client_icon: Optional[str] = None, client_icon: str | None = None,
token_type: str = models.TOKEN_TYPE_NORMAL, token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
credential: Optional[models.Credentials] = None, credential: models.Credentials | None = None,
) -> models.RefreshToken: ) -> models.RefreshToken:
"""Create a new token for a user.""" """Create a new token for a user."""
kwargs: Dict[str, Any] = { kwargs: dict[str, Any] = {
"user": user, "user": user,
"client_id": client_id, "client_id": client_id,
"token_type": token_type, "token_type": token_type,
@ -244,7 +246,7 @@ class AuthStore:
async def async_get_refresh_token( async def async_get_refresh_token(
self, token_id: str self, token_id: str
) -> Optional[models.RefreshToken]: ) -> models.RefreshToken | None:
"""Get refresh token by id.""" """Get refresh token by id."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -259,7 +261,7 @@ class AuthStore:
async def async_get_refresh_token_by_token( async def async_get_refresh_token_by_token(
self, token: str self, token: str
) -> Optional[models.RefreshToken]: ) -> models.RefreshToken | None:
"""Get refresh token by token.""" """Get refresh token by token."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -276,7 +278,7 @@ class AuthStore:
@callback @callback
def async_log_refresh_token_usage( 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: ) -> None:
"""Update refresh token last used information.""" """Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow() refresh_token.last_used_at = dt_util.utcnow()
@ -309,9 +311,9 @@ class AuthStore:
self._set_defaults() self._set_defaults()
return return
users: Dict[str, models.User] = OrderedDict() users: dict[str, models.User] = OrderedDict()
groups: Dict[str, models.Group] = OrderedDict() groups: dict[str, models.Group] = OrderedDict()
credentials: Dict[str, models.Credentials] = OrderedDict() credentials: dict[str, models.Credentials] = OrderedDict()
# Soft-migrating data as we load. We are going to make sure we have a # 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 # read only group and an admin group. There are two states that we can
@ -328,7 +330,7 @@ class AuthStore:
# was added. # was added.
for group_dict in data.get("groups", []): for group_dict in data.get("groups", []):
policy: Optional[PolicyType] = None policy: PolicyType | None = None
if group_dict["id"] == GROUP_ID_ADMIN: if group_dict["id"] == GROUP_ID_ADMIN:
has_admin_group = True has_admin_group = True
@ -489,7 +491,7 @@ class AuthStore:
self._store.async_delay_save(self._data_to_save, 1) self._store.async_delay_save(self._data_to_save, 1)
@callback @callback
def _data_to_save(self) -> Dict: def _data_to_save(self) -> dict:
"""Return the data to store.""" """Return the data to store."""
assert self._users is not None assert self._users is not None
assert self._groups is not None assert self._groups is not None
@ -508,7 +510,7 @@ class AuthStore:
groups = [] groups = []
for group in self._groups.values(): for group in self._groups.values():
g_dict: Dict[str, Any] = { g_dict: dict[str, Any] = {
"id": group.id, "id": group.id,
# Name not read for sys groups. Kept here for backwards compat # Name not read for sys groups. Kept here for backwards compat
"name": group.name, "name": group.name,
@ -567,7 +569,7 @@ class AuthStore:
"""Set default values for auth store.""" """Set default values for auth store."""
self._users = OrderedDict() self._users = OrderedDict()
groups: Dict[str, models.Group] = OrderedDict() groups: dict[str, models.Group] = OrderedDict()
admin_group = _system_admin_group() admin_group = _system_admin_group()
groups[admin_group.id] = admin_group groups[admin_group.id] = admin_group
user_group = _system_user_group() user_group = _system_user_group()

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import importlib import importlib
import logging import logging
import types import types
from typing import Any, Dict, Optional from typing import Any
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
@ -38,7 +38,7 @@ class MultiFactorAuthModule:
DEFAULT_TITLE = "Unnamed auth module" DEFAULT_TITLE = "Unnamed auth module"
MAX_RETRY_TIME = 3 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.""" """Initialize an auth module."""
self.hass = hass self.hass = hass
self.config = config self.config = config
@ -87,7 +87,7 @@ class MultiFactorAuthModule:
"""Return whether user is setup.""" """Return whether user is setup."""
raise NotImplementedError 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.""" """Return True if validation passed."""
raise NotImplementedError raise NotImplementedError
@ -104,14 +104,14 @@ class SetupFlow(data_entry_flow.FlowHandler):
self._user_id = user_id self._user_id = user_id
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the first step of setup flow. """Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input is None. Return self.async_show_form(step_id='init') if user_input is None.
Return self.async_create_entry(data={'result': result}) if finish. Return self.async_create_entry(data={'result': result}) if finish.
""" """
errors: Dict[str, str] = {} errors: dict[str, str] = {}
if user_input: if user_input:
result = await self._auth_module.async_setup_user(self._user_id, 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( async def auth_mfa_module_from_config(
hass: HomeAssistant, config: Dict[str, Any] hass: HomeAssistant, config: dict[str, Any]
) -> MultiFactorAuthModule: ) -> MultiFactorAuthModule:
"""Initialize an auth module from a config.""" """Initialize an auth module from a config."""
module_name = config[CONF_TYPE] module_name = config[CONF_TYPE]

View File

@ -1,5 +1,7 @@
"""Example auth module.""" """Example auth module."""
from typing import Any, Dict from __future__ import annotations
from typing import Any
import voluptuous as vol import voluptuous as vol
@ -28,7 +30,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
DEFAULT_TITLE = "Insecure Personal Identify Number" 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.""" """Initialize the user data store."""
super().__init__(hass, config) super().__init__(hass, config)
self._data = config["data"] self._data = config["data"]
@ -75,17 +77,11 @@ class InsecureExampleModule(MultiFactorAuthModule):
async def async_is_user_setup(self, user_id: str) -> bool: async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup.""" """Return whether user is setup."""
for data in self._data: return any(data["user_id"] == user_id for data in self._data)
if data["user_id"] == user_id:
return True
return False
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.""" """Return True if validation passed."""
for data in self._data: return any(
if data["user_id"] == user_id: data["user_id"] == user_id and data["pin"] == user_input["pin"]
# user_input has been validate in caller for data in self._data
if data["pin"] == user_input["pin"]: )
return True
return False

View File

@ -2,10 +2,12 @@
Sending HOTP through notify service Sending HOTP through notify service
""" """
from __future__ import annotations
import asyncio import asyncio
from collections import OrderedDict from collections import OrderedDict
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict
import attr import attr
import voluptuous as vol import voluptuous as vol
@ -79,8 +81,8 @@ class NotifySetting:
secret: str = attr.ib(factory=_generate_secret) # not persistent secret: str = attr.ib(factory=_generate_secret) # not persistent
counter: int = attr.ib(factory=_generate_random) # not persistent counter: int = attr.ib(factory=_generate_random) # not persistent
notify_service: Optional[str] = attr.ib(default=None) notify_service: str | None = attr.ib(default=None)
target: Optional[str] = attr.ib(default=None) target: str | None = attr.ib(default=None)
_UsersDict = Dict[str, NotifySetting] _UsersDict = Dict[str, NotifySetting]
@ -92,10 +94,10 @@ class NotifyAuthModule(MultiFactorAuthModule):
DEFAULT_TITLE = "Notify One-Time Password" 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.""" """Initialize the user data store."""
super().__init__(hass, config) super().__init__(hass, config)
self._user_settings: Optional[_UsersDict] = None self._user_settings: _UsersDict | None = None
self._user_store = hass.helpers.storage.Store( self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True STORAGE_VERSION, STORAGE_KEY, private=True
) )
@ -146,7 +148,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
) )
@callback @callback
def aync_get_available_notify_services(self) -> List[str]: def aync_get_available_notify_services(self) -> list[str]:
"""Return list of notify services.""" """Return list of notify services."""
unordered_services = set() unordered_services = set()
@ -198,7 +200,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
return user_id in self._user_settings 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.""" """Return True if validation passed."""
if self._user_settings is None: if self._user_settings is None:
await self._async_load() await self._async_load()
@ -258,7 +260,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
) )
async def async_notify( 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: ) -> None:
"""Send code by notify service.""" """Send code by notify service."""
data = {"message": self._message_template.format(code)} data = {"message": self._message_template.format(code)}
@ -276,23 +278,23 @@ class NotifySetupFlow(SetupFlow):
auth_module: NotifyAuthModule, auth_module: NotifyAuthModule,
setup_schema: vol.Schema, setup_schema: vol.Schema,
user_id: str, user_id: str,
available_notify_services: List[str], available_notify_services: list[str],
) -> None: ) -> None:
"""Initialize the setup flow.""" """Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user_id) super().__init__(auth_module, setup_schema, user_id)
# to fix typing complaint # to fix typing complaint
self._auth_module: NotifyAuthModule = auth_module self._auth_module: NotifyAuthModule = auth_module
self._available_notify_services = available_notify_services self._available_notify_services = available_notify_services
self._secret: Optional[str] = None self._secret: str | None = None
self._count: Optional[int] = None self._count: int | None = None
self._notify_service: Optional[str] = None self._notify_service: str | None = None
self._target: Optional[str] = None self._target: str | None = None
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Let user select available notify services.""" """Let user select available notify services."""
errors: Dict[str, str] = {} errors: dict[str, str] = {}
hass = self._auth_module.hass hass = self._auth_module.hass
if user_input: if user_input:
@ -306,7 +308,7 @@ class NotifySetupFlow(SetupFlow):
if not self._available_notify_services: if not self._available_notify_services:
return self.async_abort(reason="no_available_service") 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["notify_service"] = vol.In(self._available_notify_services)
schema["target"] = vol.Optional(str) schema["target"] = vol.Optional(str)
@ -315,10 +317,10 @@ class NotifySetupFlow(SetupFlow):
) )
async def async_step_setup( async def async_step_setup(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Verify user can receive one-time password.""" """Verify user can receive one-time password."""
errors: Dict[str, str] = {} errors: dict[str, str] = {}
hass = self._auth_module.hass hass = self._auth_module.hass
if user_input: if user_input:

View File

@ -1,7 +1,9 @@
"""Time-based One Time Password auth module.""" """Time-based One Time Password auth module."""
from __future__ import annotations
import asyncio import asyncio
from io import BytesIO from io import BytesIO
from typing import Any, Dict, Optional, Tuple from typing import Any
import voluptuous as vol 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.""" """Generate a secret, url, and QR code."""
import pyotp # pylint: disable=import-outside-toplevel import pyotp # pylint: disable=import-outside-toplevel
@ -69,10 +71,10 @@ class TotpAuthModule(MultiFactorAuthModule):
DEFAULT_TITLE = "Time-based One Time Password" DEFAULT_TITLE = "Time-based One Time Password"
MAX_RETRY_TIME = 5 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.""" """Initialize the user data store."""
super().__init__(hass, config) 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( self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True STORAGE_VERSION, STORAGE_KEY, private=True
) )
@ -100,7 +102,7 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Save data.""" """Save data."""
await self._user_store.async_save({STORAGE_USERS: self._users}) 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.""" """Create a ota_secret for user."""
import pyotp # pylint: disable=import-outside-toplevel import pyotp # pylint: disable=import-outside-toplevel
@ -145,7 +147,7 @@ class TotpAuthModule(MultiFactorAuthModule):
return user_id in self._users # type: ignore 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.""" """Return True if validation passed."""
if self._users is None: if self._users is None:
await self._async_load() await self._async_load()
@ -181,13 +183,13 @@ class TotpSetupFlow(SetupFlow):
# to fix typing complaint # to fix typing complaint
self._auth_module: TotpAuthModule = auth_module self._auth_module: TotpAuthModule = auth_module
self._user = user self._user = user
self._ota_secret: Optional[str] = None self._ota_secret: str | None = None
self._url = None # type Optional[str] self._url = None # type Optional[str]
self._image = None # type Optional[str] self._image = None # type Optional[str]
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the first step of setup flow. """Handle the first step of setup flow.
Return self.async_show_form(step_id='init') if user_input is None. 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 import pyotp # pylint: disable=import-outside-toplevel
errors: Dict[str, str] = {} errors: dict[str, str] = {}
if user_input: if user_input:
verified = await self.hass.async_add_executor_job( verified = await self.hass.async_add_executor_job(

View File

@ -1,7 +1,9 @@
"""Auth models.""" """Auth models."""
from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
import secrets import secrets
from typing import Dict, List, NamedTuple, Optional from typing import NamedTuple
import uuid import uuid
import attr import attr
@ -21,7 +23,7 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
class Group: class Group:
"""A group.""" """A group."""
name: Optional[str] = attr.ib() name: str | None = attr.ib()
policy: perm_mdl.PolicyType = attr.ib() policy: perm_mdl.PolicyType = attr.ib()
id: str = attr.ib(factory=lambda: uuid.uuid4().hex) id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
system_generated: bool = attr.ib(default=False) system_generated: bool = attr.ib(default=False)
@ -31,24 +33,24 @@ class Group:
class User: class User:
"""A user.""" """A user."""
name: Optional[str] = attr.ib() name: str | None = attr.ib()
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False) perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
id: str = attr.ib(factory=lambda: uuid.uuid4().hex) id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
is_owner: bool = attr.ib(default=False) is_owner: bool = attr.ib(default=False)
is_active: bool = attr.ib(default=False) is_active: bool = attr.ib(default=False)
system_generated: 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. # 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. # 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 factory=dict, eq=False, order=False
) )
_permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib( _permissions: perm_mdl.PolicyPermissions | None = attr.ib(
init=False, init=False,
eq=False, eq=False,
order=False, order=False,
@ -89,10 +91,10 @@ class RefreshToken:
"""RefreshToken for a user to grant new access tokens.""" """RefreshToken for a user to grant new access tokens."""
user: User = attr.ib() user: User = attr.ib()
client_id: Optional[str] = attr.ib() client_id: str | None = attr.ib()
access_token_expiration: timedelta = attr.ib() access_token_expiration: timedelta = attr.ib()
client_name: Optional[str] = attr.ib(default=None) client_name: str | None = attr.ib(default=None)
client_icon: Optional[str] = attr.ib(default=None) client_icon: str | None = attr.ib(default=None)
token_type: str = attr.ib( token_type: str = attr.ib(
default=TOKEN_TYPE_NORMAL, default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_( validator=attr.validators.in_(
@ -104,12 +106,12 @@ class RefreshToken:
token: str = attr.ib(factory=lambda: secrets.token_hex(64)) token: str = attr.ib(factory=lambda: secrets.token_hex(64))
jwt_key: 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_at: datetime | None = attr.ib(default=None)
last_used_ip: Optional[str] = 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) @attr.s(slots=True)
@ -117,7 +119,7 @@ class Credentials:
"""Credentials for a user on an auth provider.""" """Credentials for a user on an auth provider."""
auth_provider_type: str = attr.ib() 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. # Allow the auth provider to store data to represent their auth.
data: dict = attr.ib() data: dict = attr.ib()
@ -129,5 +131,5 @@ class Credentials:
class UserMeta(NamedTuple): class UserMeta(NamedTuple):
"""User metadata.""" """User metadata."""
name: Optional[str] name: str | None
is_active: bool is_active: bool

View File

@ -1,6 +1,8 @@
"""Permissions for Home Assistant.""" """Permissions for Home Assistant."""
from __future__ import annotations
import logging import logging
from typing import Any, Callable, Optional from typing import Any, Callable
import voluptuous as vol import voluptuous as vol
@ -19,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
class AbstractPermissions: class AbstractPermissions:
"""Default permissions class.""" """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]: def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access.""" """Return a function that can test entity access."""

View File

@ -1,6 +1,8 @@
"""Entity permissions.""" """Entity permissions."""
from __future__ import annotations
from collections import OrderedDict from collections import OrderedDict
from typing import Callable, Optional from typing import Callable
import voluptuous as vol import voluptuous as vol
@ -43,14 +45,14 @@ ENTITY_POLICY_SCHEMA = vol.Any(
def _lookup_domain( def _lookup_domain(
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]: ) -> ValueType | None:
"""Look up entity permissions by domain.""" """Look up entity permissions by domain."""
return domains_dict.get(entity_id.split(".", 1)[0]) return domains_dict.get(entity_id.split(".", 1)[0])
def _lookup_area( def _lookup_area(
perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]: ) -> ValueType | None:
"""Look up entity permissions by area.""" """Look up entity permissions by area."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id) entity_entry = perm_lookup.entity_registry.async_get(entity_id)
@ -67,7 +69,7 @@ def _lookup_area(
def _lookup_device( def _lookup_device(
perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]: ) -> ValueType | None:
"""Look up entity permissions by device.""" """Look up entity permissions by device."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id) entity_entry = perm_lookup.entity_registry.async_get(entity_id)
@ -79,7 +81,7 @@ def _lookup_device(
def _lookup_entity_id( def _lookup_entity_id(
perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]: ) -> ValueType | None:
"""Look up entity permission by entity id.""" """Look up entity permission by entity id."""
return entities_dict.get(entity_id) return entities_dict.get(entity_id)

View File

@ -1,13 +1,15 @@
"""Merging of policies.""" """Merging of policies."""
from typing import Dict, List, Set, cast from __future__ import annotations
from typing import cast
from .types import CategoryType, PolicyType from .types import CategoryType, PolicyType
def merge_policies(policies: List[PolicyType]) -> PolicyType: def merge_policies(policies: list[PolicyType]) -> PolicyType:
"""Merge policies.""" """Merge policies."""
new_policy: Dict[str, CategoryType] = {} new_policy: dict[str, CategoryType] = {}
seen: Set[str] = set() seen: set[str] = set()
for policy in policies: for policy in policies:
for category in policy: for category in policy:
if category in seen: if category in seen:
@ -20,7 +22,7 @@ def merge_policies(policies: List[PolicyType]) -> PolicyType:
return new_policy return new_policy
def _merge_policies(sources: List[CategoryType]) -> CategoryType: def _merge_policies(sources: list[CategoryType]) -> CategoryType:
"""Merge a policy.""" """Merge a policy."""
# When merging policies, the most permissive wins. # When merging policies, the most permissive wins.
# This means we order it like this: # This means we order it like this:
@ -34,7 +36,7 @@ def _merge_policies(sources: List[CategoryType]) -> CategoryType:
# merge each key in the source. # merge each key in the source.
policy: CategoryType = None policy: CategoryType = None
seen: Set[str] = set() seen: set[str] = set()
for source in sources: for source in sources:
if source is None: if source is None:
continue continue

View File

@ -1,11 +1,12 @@
"""Models for permissions.""" """Models for permissions."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import attr import attr
if TYPE_CHECKING: if TYPE_CHECKING:
# pylint: disable=unused-import from homeassistant.helpers import (
from homeassistant.helpers import ( # noqa: F401
device_registry as dev_reg, device_registry as dev_reg,
entity_registry as ent_reg, entity_registry as ent_reg,
) )
@ -15,5 +16,5 @@ if TYPE_CHECKING:
class PermissionLookup: class PermissionLookup:
"""Class to hold data for permission lookups.""" """Class to hold data for permission lookups."""
entity_registry: "ent_reg.EntityRegistry" = attr.ib() entity_registry: ent_reg.EntityRegistry = attr.ib()
device_registry: "dev_reg.DeviceRegistry" = attr.ib() device_registry: dev_reg.DeviceRegistry = attr.ib()

View File

@ -1,6 +1,8 @@
"""Helpers to deal with permissions.""" """Helpers to deal with permissions."""
from __future__ import annotations
from functools import wraps 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 .const import SUBCAT_ALL
from .models import PermissionLookup from .models import PermissionLookup
@ -45,7 +47,7 @@ def compile_policy(
assert isinstance(policy, dict) 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(): for key, lookup_func in subcategories.items():
lookup_value = policy.get(key) lookup_value = policy.get(key)
@ -80,10 +82,10 @@ def compile_policy(
def _gen_dict_test_func( def _gen_dict_test_func(
perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict
) -> Callable[[str, str], Optional[bool]]: ) -> Callable[[str, str], bool | None]:
"""Generate a lookup function.""" """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.""" """Test if permission is allowed based on the keys."""
schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id) schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id)

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import importlib import importlib
import logging import logging
import types import types
from typing import Any, Dict, List, Optional from typing import Any
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
@ -42,7 +42,7 @@ class AuthProvider:
DEFAULT_TITLE = "Unnamed auth provider" DEFAULT_TITLE = "Unnamed auth provider"
def __init__( def __init__(
self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] self, hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
) -> None: ) -> None:
"""Initialize an auth provider.""" """Initialize an auth provider."""
self.hass = hass self.hass = hass
@ -50,7 +50,7 @@ class AuthProvider:
self.config = config self.config = config
@property @property
def id(self) -> Optional[str]: def id(self) -> str | None:
"""Return id of the auth provider. """Return id of the auth provider.
Optional, can be None. Optional, can be None.
@ -72,7 +72,7 @@ class AuthProvider:
"""Return whether multi-factor auth supported by the auth provider.""" """Return whether multi-factor auth supported by the auth provider."""
return True return True
async def async_credentials(self) -> List[Credentials]: async def async_credentials(self) -> list[Credentials]:
"""Return all credentials of this provider.""" """Return all credentials of this provider."""
users = await self.store.async_get_users() users = await self.store.async_get_users()
return [ return [
@ -86,7 +86,7 @@ class AuthProvider:
] ]
@callback @callback
def async_create_credentials(self, data: Dict[str, str]) -> Credentials: def async_create_credentials(self, data: dict[str, str]) -> Credentials:
"""Create credentials.""" """Create credentials."""
return Credentials( return Credentials(
auth_provider_type=self.type, auth_provider_id=self.id, data=data auth_provider_type=self.type, auth_provider_id=self.id, data=data
@ -94,7 +94,7 @@ class AuthProvider:
# Implement by extending class # 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. """Return the data flow for logging in with auth provider.
Auth provider should extend LoginFlow and return an instance. Auth provider should extend LoginFlow and return an instance.
@ -102,7 +102,7 @@ class AuthProvider:
raise NotImplementedError raise NotImplementedError
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str] self, flow_result: dict[str, str]
) -> Credentials: ) -> Credentials:
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
raise NotImplementedError raise NotImplementedError
@ -121,7 +121,7 @@ class AuthProvider:
@callback @callback
def async_validate_refresh_token( def async_validate_refresh_token(
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None self, refresh_token: RefreshToken, remote_ip: str | None = None
) -> None: ) -> None:
"""Verify a refresh token is still valid. """Verify a refresh token is still valid.
@ -131,7 +131,7 @@ class AuthProvider:
async def auth_provider_from_config( async def auth_provider_from_config(
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any] hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
) -> AuthProvider: ) -> AuthProvider:
"""Initialize an auth provider from a config.""" """Initialize an auth provider from a config."""
provider_name = config[CONF_TYPE] provider_name = config[CONF_TYPE]
@ -188,17 +188,17 @@ class LoginFlow(data_entry_flow.FlowHandler):
def __init__(self, auth_provider: AuthProvider) -> None: def __init__(self, auth_provider: AuthProvider) -> None:
"""Initialize the login flow.""" """Initialize the login flow."""
self._auth_provider = auth_provider 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._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.created_at = dt_util.utcnow()
self.invalid_mfa_times = 0 self.invalid_mfa_times = 0
self.user: Optional[User] = None self.user: User | None = None
self.credential: Optional[Credentials] = None self.credential: Credentials | None = None
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the first step of login flow. """Handle the first step of login flow.
Return self.async_show_form(step_id='init') if user_input is None. 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 raise NotImplementedError
async def async_step_select_mfa_module( async def async_step_select_mfa_module(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of select mfa module.""" """Handle the step of select mfa module."""
errors = {} errors = {}
@ -232,8 +232,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
) )
async def async_step_mfa( async def async_step_mfa(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of mfa validation.""" """Handle the step of mfa validation."""
assert self.credential assert self.credential
assert self.user assert self.user
@ -273,7 +273,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
if not errors: if not errors:
return await self.async_finish(self.credential) 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_name": auth_module.name,
"mfa_module_id": auth_module.id, "mfa_module_id": auth_module.id,
} }
@ -285,6 +285,6 @@ class LoginFlow(data_entry_flow.FlowHandler):
errors=errors, 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.""" """Handle the pass of login flow."""
return self.async_create_entry(title=self._auth_provider.name, data=flow_result) return self.async_create_entry(title=self._auth_provider.name, data=flow_result)

View File

@ -1,10 +1,11 @@
"""Auth provider that validates credentials via an external command.""" """Auth provider that validates credentials via an external command."""
from __future__ import annotations
import asyncio.subprocess import asyncio.subprocess
import collections import collections
import logging import logging
import os import os
from typing import Any, Dict, Optional, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
@ -51,9 +52,9 @@ class CommandLineAuthProvider(AuthProvider):
attributes provided by external programs. attributes provided by external programs.
""" """
super().__init__(*args, **kwargs) 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 a flow to login."""
return CommandLineLoginFlow(self) return CommandLineLoginFlow(self)
@ -82,7 +83,7 @@ class CommandLineAuthProvider(AuthProvider):
raise InvalidAuthError raise InvalidAuthError
if self.config[CONF_META]: if self.config[CONF_META]:
meta: Dict[str, str] = {} meta: dict[str, str] = {}
for _line in stdout.splitlines(): for _line in stdout.splitlines():
try: try:
line = _line.decode().lstrip() line = _line.decode().lstrip()
@ -99,7 +100,7 @@ class CommandLineAuthProvider(AuthProvider):
self._user_meta[username] = meta self._user_meta[username] = meta
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str] self, flow_result: dict[str, str]
) -> Credentials: ) -> Credentials:
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
username = flow_result["username"] username = flow_result["username"]
@ -125,8 +126,8 @@ class CommandLineLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}
@ -143,7 +144,7 @@ class CommandLineLoginFlow(LoginFlow):
user_input.pop("password") user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema: Dict[str, type] = collections.OrderedDict() schema: dict[str, type] = collections.OrderedDict()
schema["username"] = str schema["username"] = str
schema["password"] = str schema["password"] = str

View File

@ -5,7 +5,7 @@ import asyncio
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
import logging import logging
from typing import Any, Dict, List, Optional, Set, cast from typing import Any, cast
import bcrypt import bcrypt
import voluptuous as vol import voluptuous as vol
@ -21,7 +21,7 @@ STORAGE_VERSION = 1
STORAGE_KEY = "auth_provider.homeassistant" 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.""" """Disallow ID in config."""
if CONF_ID in conf: if CONF_ID in conf:
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.") raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
@ -62,7 +62,7 @@ class Data:
self._store = hass.helpers.storage.Store( self._store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True 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 # Legacy mode will allow usernames to start/end with whitespace
# and will compare usernames case-insensitive. # and will compare usernames case-insensitive.
# Remove in 2020 or when we launch 1.0. # Remove in 2020 or when we launch 1.0.
@ -83,7 +83,7 @@ class Data:
if data is None: if data is None:
data = {"users": []} data = {"users": []}
seen: Set[str] = set() seen: set[str] = set()
for user in data["users"]: for user in data["users"]:
username = user["username"] username = user["username"]
@ -121,7 +121,7 @@ class Data:
self._data = data self._data = data
@property @property
def users(self) -> List[Dict[str, str]]: def users(self) -> list[dict[str, str]]:
"""Return users.""" """Return users."""
return self._data["users"] # type: ignore return self._data["users"] # type: ignore
@ -220,7 +220,7 @@ class HassAuthProvider(AuthProvider):
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize an Home Assistant auth provider.""" """Initialize an Home Assistant auth provider."""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.data: Optional[Data] = None self.data: Data | None = None
self._init_lock = asyncio.Lock() self._init_lock = asyncio.Lock()
async def async_initialize(self) -> None: async def async_initialize(self) -> None:
@ -233,7 +233,7 @@ class HassAuthProvider(AuthProvider):
await data.async_load() await data.async_load()
self.data = data 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 a flow to login."""
return HassLoginFlow(self) return HassLoginFlow(self)
@ -277,7 +277,7 @@ class HassAuthProvider(AuthProvider):
await self.data.async_save() await self.data.async_save()
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str] self, flow_result: dict[str, str]
) -> Credentials: ) -> Credentials:
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
if self.data is None: if self.data is None:
@ -318,8 +318,8 @@ class HassLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}
@ -335,7 +335,7 @@ class HassLoginFlow(LoginFlow):
user_input.pop("password") user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema: Dict[str, type] = OrderedDict() schema: dict[str, type] = OrderedDict()
schema["username"] = str schema["username"] = str
schema["password"] = str schema["password"] = str

View File

@ -1,7 +1,9 @@
"""Example auth provider.""" """Example auth provider."""
from __future__ import annotations
from collections import OrderedDict from collections import OrderedDict
import hmac import hmac
from typing import Any, Dict, Optional, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
@ -33,7 +35,7 @@ class InvalidAuthError(HomeAssistantError):
class ExampleAuthProvider(AuthProvider): class ExampleAuthProvider(AuthProvider):
"""Example auth provider based on hardcoded usernames and passwords.""" """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 a flow to login."""
return ExampleLoginFlow(self) return ExampleLoginFlow(self)
@ -60,7 +62,7 @@ class ExampleAuthProvider(AuthProvider):
raise InvalidAuthError raise InvalidAuthError
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str] self, flow_result: dict[str, str]
) -> Credentials: ) -> Credentials:
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
username = flow_result["username"] username = flow_result["username"]
@ -94,8 +96,8 @@ class ExampleLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}
@ -111,7 +113,7 @@ class ExampleLoginFlow(LoginFlow):
user_input.pop("password") user_input.pop("password")
return await self.async_finish(user_input) return await self.async_finish(user_input)
schema: Dict[str, type] = OrderedDict() schema: dict[str, type] = OrderedDict()
schema["username"] = str schema["username"] = str
schema["password"] = str schema["password"] = str

View File

@ -3,8 +3,10 @@ Support Legacy API password auth provider.
It will be removed when auth system production ready It will be removed when auth system production ready
""" """
from __future__ import annotations
import hmac import hmac
from typing import Any, Dict, Optional, cast from typing import Any, cast
import voluptuous as vol import voluptuous as vol
@ -40,7 +42,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
"""Return api_password.""" """Return api_password."""
return str(self.config[CONF_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 a flow to login."""
return LegacyLoginFlow(self) return LegacyLoginFlow(self)
@ -55,7 +57,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
raise InvalidAuthError raise InvalidAuthError
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str] self, flow_result: dict[str, str]
) -> Credentials: ) -> Credentials:
"""Return credentials for this login.""" """Return credentials for this login."""
credentials = await self.async_credentials() credentials = await self.async_credentials()
@ -79,8 +81,8 @@ class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow.""" """Handler for the login flow."""
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
errors = {} errors = {}

View File

@ -3,6 +3,8 @@
It shows list of users if access from trusted network. It shows list of users if access from trusted network.
Abort login flow if not access from trusted network. Abort login flow if not access from trusted network.
""" """
from __future__ import annotations
from ipaddress import ( from ipaddress import (
IPv4Address, IPv4Address,
IPv4Network, IPv4Network,
@ -11,7 +13,7 @@ from ipaddress import (
ip_address, ip_address,
ip_network, ip_network,
) )
from typing import Any, Dict, List, Optional, Union, cast from typing import Any, Dict, List, Union, cast
import voluptuous as vol import voluptuous as vol
@ -68,12 +70,12 @@ class TrustedNetworksAuthProvider(AuthProvider):
DEFAULT_TITLE = "Trusted Networks" DEFAULT_TITLE = "Trusted Networks"
@property @property
def trusted_networks(self) -> List[IPNetwork]: def trusted_networks(self) -> list[IPNetwork]:
"""Return trusted networks.""" """Return trusted networks."""
return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS]) return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS])
@property @property
def trusted_users(self) -> Dict[IPNetwork, Any]: def trusted_users(self) -> dict[IPNetwork, Any]:
"""Return trusted users per network.""" """Return trusted users per network."""
return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS]) 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.""" """Trusted Networks auth provider does not support MFA."""
return False 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.""" """Return a flow to login."""
assert context is not None assert context is not None
ip_addr = cast(IPAddress, context.get("ip_address")) ip_addr = cast(IPAddress, context.get("ip_address"))
@ -111,7 +113,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
if ( if (
user.id in user_list user.id in user_list
or any( 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( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str] self, flow_result: dict[str, str]
) -> Credentials: ) -> Credentials:
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
user_id = flow_result["user"] user_id = flow_result["user"]
@ -169,7 +171,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
@callback @callback
def async_validate_refresh_token( def async_validate_refresh_token(
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None self, refresh_token: RefreshToken, remote_ip: str | None = None
) -> None: ) -> None:
"""Verify a refresh token is still valid.""" """Verify a refresh token is still valid."""
if remote_ip is None: if remote_ip is None:
@ -186,7 +188,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
self, self,
auth_provider: TrustedNetworksAuthProvider, auth_provider: TrustedNetworksAuthProvider,
ip_addr: IPAddress, ip_addr: IPAddress,
available_users: Dict[str, Optional[str]], available_users: dict[str, str | None],
allow_bypass_login: bool, allow_bypass_login: bool,
) -> None: ) -> None:
"""Initialize the login flow.""" """Initialize the login flow."""
@ -196,8 +198,8 @@ class TrustedNetworksLoginFlow(LoginFlow):
self._allow_bypass_login = allow_bypass_login self._allow_bypass_login = allow_bypass_login
async def async_step_init( async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None self, user_input: dict[str, str] | None = None
) -> Dict[str, Any]: ) -> dict[str, Any]:
"""Handle the step of the form.""" """Handle the step of the form."""
try: try:
cast( cast(

View File

@ -1,4 +1,6 @@
"""Provide methods to bootstrap a Home Assistant instance.""" """Provide methods to bootstrap a Home Assistant instance."""
from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
from datetime import datetime from datetime import datetime
@ -8,7 +10,7 @@ import os
import sys import sys
import threading import threading
from time import monotonic from time import monotonic
from typing import TYPE_CHECKING, Any, Dict, Optional, Set from typing import TYPE_CHECKING, Any
import voluptuous as vol import voluptuous as vol
import yarl import yarl
@ -28,7 +30,6 @@ from homeassistant.setup import (
from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.async_ import gather_with_concurrency
from homeassistant.util.logging import async_activate_log_queue_handler 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.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
if TYPE_CHECKING: if TYPE_CHECKING:
from .runner import RuntimeConfig from .runner import RuntimeConfig
@ -75,8 +76,8 @@ STAGE_1_INTEGRATIONS = {
async def async_setup_hass( async def async_setup_hass(
runtime_config: "RuntimeConfig", runtime_config: RuntimeConfig,
) -> Optional[core.HomeAssistant]: ) -> core.HomeAssistant | None:
"""Set up Home Assistant.""" """Set up Home Assistant."""
hass = core.HomeAssistant() hass = core.HomeAssistant()
hass.config.config_dir = runtime_config.config_dir hass.config.config_dir = runtime_config.config_dir
@ -122,8 +123,6 @@ async def async_setup_hass(
basic_setup_success = ( basic_setup_success = (
await async_from_config_dict(config_dict, hass) is not None await async_from_config_dict(config_dict, hass) is not None
) )
finally:
clear_secret_cache()
if config_dict is None: if config_dict is None:
safe_mode = True safe_mode = True
@ -191,7 +190,7 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
async def async_from_config_dict( async def async_from_config_dict(
config: ConfigType, hass: core.HomeAssistant config: ConfigType, hass: core.HomeAssistant
) -> Optional[core.HomeAssistant]: ) -> core.HomeAssistant | None:
"""Try to configure Home Assistant from a configuration dictionary. """Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies. Dynamically loads required components and its dependencies.
@ -258,8 +257,8 @@ async def async_from_config_dict(
def async_enable_logging( def async_enable_logging(
hass: core.HomeAssistant, hass: core.HomeAssistant,
verbose: bool = False, verbose: bool = False,
log_rotate_days: Optional[int] = None, log_rotate_days: int | None = None,
log_file: Optional[str] = None, log_file: str | None = None,
log_no_color: bool = False, log_no_color: bool = False,
) -> None: ) -> None:
"""Set up the logging. """Set up the logging.
@ -365,7 +364,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
@core.callback @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.""" """Get domains of components to set up."""
# Filter out the repeating and common config section [homeassistant] # Filter out the repeating and common config section [homeassistant]
domains = {key.split(" ")[0] for key in config if key != core.DOMAIN} 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( 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: ) -> None:
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
while True: while True:
@ -399,9 +398,9 @@ async def _async_log_pending_setups(
async def async_setup_multi_components( async def async_setup_multi_components(
hass: core.HomeAssistant, hass: core.HomeAssistant,
domains: Set[str], domains: set[str],
config: Dict[str, Any], config: dict[str, Any],
setup_started: Dict[str, datetime], setup_started: dict[str, datetime],
) -> None: ) -> None:
"""Set up multiple domains. Log on failure.""" """Set up multiple domains. Log on failure."""
futures = { futures = {
@ -425,7 +424,7 @@ async def async_setup_multi_components(
async def _async_set_up_integrations( async def _async_set_up_integrations(
hass: core.HomeAssistant, config: Dict[str, Any] hass: core.HomeAssistant, config: dict[str, Any]
) -> None: ) -> None:
"""Set up all the integrations.""" """Set up all the integrations."""
setup_started = hass.data[DATA_SETUP_STARTED] = {} 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 # Resolve all dependencies so we know all integrations
# that will have to be loaded and start rightaway # that will have to be loaded and start rightaway
integration_cache: Dict[str, loader.Integration] = {} integration_cache: dict[str, loader.Integration] = {}
to_resolve = domains_to_setup to_resolve = domains_to_setup
while to_resolve: while to_resolve:
old_to_resolve = to_resolve old_to_resolve = to_resolve

View File

@ -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}) AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
ABODE_PLATFORMS = [ PLATFORMS = [
"alarm_control_panel", "alarm_control_panel",
"binary_sensor", "binary_sensor",
"lock", "lock",
@ -138,7 +138,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN] = AbodeSystem(abode, polling) hass.data[DOMAIN] = AbodeSystem(abode, polling)
for platform in ABODE_PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform) hass.config_entries.async_forward_entry_setup(config_entry, platform)
) )
@ -158,7 +158,7 @@ async def async_unload_entry(hass, config_entry):
tasks = [] tasks = []
for platform in ABODE_PLATFORMS: for platform in PLATFORMS:
tasks.append( tasks.append(
hass.config_entries.async_forward_entry_unload(config_entry, platform) hass.config_entries.async_forward_entry_unload(config_entry, platform)
) )
@ -363,7 +363,7 @@ class AbodeDevice(AbodeEntity):
return self._device.name return self._device.name
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,
@ -411,7 +411,7 @@ class AbodeAutomation(AbodeEntity):
return self._automation.name return self._automation.name
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"} return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"}

View File

@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
self._device.set_away() self._device.set_away()
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,

View File

@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST 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_MFA = "mfa_code"
CONF_POLLING = "polling" CONF_POLLING = "polling"
@ -163,7 +163,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_config): async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml.""" """Import a config entry from configuration.yaml."""
if self._async_current_entries(): 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") return self.async_abort(reason="single_instance_allowed")
self._polling = import_config.get(CONF_POLLING, False) self._polling = import_config.get(CONF_POLLING, False)

View File

@ -1,6 +1,7 @@
"""Support for Abode Security System sensors.""" """Support for Abode Security System sensors."""
import abodepy.helpers.constants as CONST import abodepy.helpers.constants as CONST
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities) async_add_entities(entities)
class AbodeSensor(AbodeDevice): class AbodeSensor(AbodeDevice, SensorEntity):
"""A sensor implementation for Abode devices.""" """A sensor implementation for Abode devices."""
def __init__(self, data, device, sensor_type): def __init__(self, data, device, sensor_type):

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "\u06a9\u0644\u0645\u0647 \u0639\u0628\u0648\u0631",
"username": "\u0627\u06cc\u0645\u06cc\u0644"
}
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
}
}
}
}
}

View File

@ -1,16 +1,25 @@
{ {
"config": { "config": {
"abort": { "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": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s", "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
"invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d" "invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d"
}, },
"step": { "step": {
"mfa": { "mfa": {
"data": { "data": {
"mfa_code": "MFA k\u00f3d (6 jegy\u0171)" "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": { "user": {

View File

@ -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"
}
}
}
}

View File

@ -2,18 +2,26 @@
"config": { "config": {
"abort": { "abort": {
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", "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": { "error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\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" "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": { "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": { "reauth_confirm": {
"data": { "data": {
"password": "\ube44\ubc00\ubc88\ud638", "password": "\ube44\ubc00\ubc88\ud638",
"username": "\uc774\uba54\uc77c" "username": "\uc774\uba54\uc77c"
} },
"title": "Abode \ub85c\uadf8\uc778 \uc815\ubcf4 \uc785\ub825\ud558\uae30"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -8,8 +8,6 @@ from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout from async_timeout import timeout
from homeassistant.const import CONF_API_KEY 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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -26,12 +24,6 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "weather"] 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: async def async_setup_entry(hass, config_entry) -> bool:
"""Set up AccuWeather as config entry.""" """Set up AccuWeather as config entry."""
api_key = config_entry.data[CONF_API_KEY] api_key = config_entry.data[CONF_API_KEY]
@ -45,21 +37,18 @@ async def async_setup_entry(hass, config_entry) -> bool:
coordinator = AccuWeatherDataUpdateCoordinator( coordinator = AccuWeatherDataUpdateCoordinator(
hass, websession, api_key, location_key, forecast hass, websession, api_key, location_key, forecast
) )
await coordinator.async_refresh() await coordinator.async_config_entry_first_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
undo_listener = config_entry.add_update_listener(update_listener) 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, COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener, UNDO_UPDATE_LISTENER: undo_listener,
} }
for component in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( 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 return True
@ -70,8 +59,8 @@ async def async_unload_entry(hass, config_entry):
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_unload(config_entry, component) hass.config_entries.async_forward_entry_unload(config_entry, platform)
for component in PLATFORMS for platform in PLATFORMS
] ]
) )
) )

View File

@ -13,7 +13,7 @@ from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv 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): class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):

View File

@ -2,7 +2,7 @@
"domain": "accuweather", "domain": "accuweather",
"name": "AccuWeather", "name": "AccuWeather",
"documentation": "https://www.home-assistant.io/integrations/accuweather/", "documentation": "https://www.home-assistant.io/integrations/accuweather/",
"requirements": ["accuweather==0.1.0"], "requirements": ["accuweather==0.1.1"],
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"config_flow": true, "config_flow": true,
"quality_scale": "platinum" "quality_scale": "platinum"

View File

@ -1,4 +1,5 @@
"""Support for the AccuWeather service.""" """Support for the AccuWeather service."""
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
@ -48,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, False) async_add_entities(sensors, False)
class AccuWeatherSensor(CoordinatorEntity): class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
def __init__(self, name, kind, coordinator, forecast_day=None): def __init__(self, name, kind, coordinator, forecast_day=None):
@ -141,7 +142,7 @@ class AccuWeatherSensor(CoordinatorEntity):
return SENSOR_TYPES[self.kind][self._unit_system] return SENSOR_TYPES[self.kind][self._unit_system]
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
if self.forecast_day is not None: if self.forecast_day is not None:
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:

View File

@ -5,7 +5,8 @@
}, },
"error": { "error": {
"cannot_connect": "Verbindung fehlgeschlagen", "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": { "step": {
"user": { "user": {

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
}
}
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -1,11 +1,12 @@
{ {
"config": { "config": {
"abort": { "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": { "error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "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": { "step": {
"user": { "user": {
@ -15,15 +16,26 @@
"longitude": "\uacbd\ub3c4", "longitude": "\uacbd\ub3c4",
"name": "\uc774\ub984" "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": { "options": {
"step": { "step": {
"user": { "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"
} }
} }
} }

View File

@ -31,5 +31,11 @@
"title": "AccuWeather-opties" "title": "AccuWeather-opties"
} }
} }
},
"system_health": {
"info": {
"can_reach_server": "Kan AccuWeather server bereiken",
"remaining_requests": "Resterende toegestane verzoeken"
}
} }
} }

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Cs\u00f6kken\u0151",
"rising": "Emelked\u0151",
"steady": "\u00c1lland\u00f3"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Turun",
"rising": "Naik",
"steady": "Tetap"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "\ud558\uac15",
"rising": "\uc0c1\uc2b9",
"steady": "\uc548\uc815"
}
}
}

View File

@ -132,7 +132,7 @@ class AcerSwitch(SwitchEntity):
return self._state return self._state
@property @property
def state_attributes(self): def extra_state_attributes(self):
"""Return state attributes.""" """Return state attributes."""
return self._attributes return self._attributes

View File

@ -11,11 +11,6 @@ CONF_HUBS = "hubs"
PLATFORMS = ["cover", "sensor"] 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( async def async_setup_entry(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
): ):
@ -28,9 +23,9 @@ async def async_setup_entry(
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = hub hass.data[DOMAIN][config_entry.entry_id] = hub
for component in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( 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 return True
@ -45,8 +40,8 @@ async def async_unload_entry(
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_unload(config_entry, component) hass.config_entries.async_forward_entry_unload(config_entry, platform)
for component in PLATFORMS for platform in PLATFORMS
] ]
) )
) )

View File

@ -1,6 +1,8 @@
"""Config flow for Rollease Acmeda Automate Pulse Hub.""" """Config flow for Rollease Acmeda Automate Pulse Hub."""
from __future__ import annotations
import asyncio import asyncio
from typing import Dict, Optional from contextlib import suppress
import aiopulse import aiopulse
import async_timeout import async_timeout
@ -8,7 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from .const import DOMAIN # pylint: disable=unused-import from .const import DOMAIN
class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -19,7 +21,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize the config flow.""" """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): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
@ -36,15 +38,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
} }
hubs = [] hubs = []
try: with suppress(asyncio.TimeoutError):
with async_timeout.timeout(5): async with async_timeout.timeout(5):
async for hub in aiopulse.Hub.discover(): async for hub in aiopulse.Hub.discover():
if hub.id not in already_configured: if hub.id not in already_configured:
hubs.append(hub) hubs.append(hub)
except asyncio.TimeoutError:
pass
if len(hubs) == 0: if not hubs:
return self.async_abort(reason="no_devices_found") return self.async_abort(reason="no_devices_found")
if len(hubs) == 1: if len(hubs) == 1:

View File

@ -1,6 +1,7 @@
"""Code to handle a Pulse Hub.""" """Code to handle a Pulse Hub."""
from __future__ import annotations
import asyncio import asyncio
from typing import Optional
import aiopulse import aiopulse
@ -17,7 +18,7 @@ class PulseHub:
"""Initialize the system.""" """Initialize the system."""
self.config_entry = config_entry self.config_entry = config_entry
self.hass = hass self.hass = hass
self.api: Optional[aiopulse.Hub] = None self.api: aiopulse.Hub | None = None
self.tasks = [] self.tasks = []
self.current_rollers = {} self.current_rollers = {}
self.cleanup_callbacks = [] self.cleanup_callbacks = []

View File

@ -1,4 +1,5 @@
"""Support for Acmeda Roller Blind Batteries.""" """Support for Acmeda Roller Blind Batteries."""
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect 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.""" """Representation of a Acmeda cover device."""
device_class = DEVICE_CLASS_BATTERY device_class = DEVICE_CLASS_BATTERY

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -53,7 +53,7 @@ class ActiontecDeviceScanner(DeviceScanner):
self.last_results = [] self.last_results = []
data = self.get_actiontec_data() data = self.get_actiontec_data()
self.success_init = data is not None self.success_init = data is not None
_LOGGER.info("canner initialized") _LOGGER.info("Scanner initialized")
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""

View File

@ -1,6 +1,8 @@
"""Support for AdGuard Home.""" """Support for AdGuard Home."""
from __future__ import annotations
import logging import logging
from typing import Any, Dict from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
import voluptuous as vol import voluptuous as vol
@ -27,11 +29,11 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,13 +45,10 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
{vol.Optional(CONF_FORCE, default=False): cv.boolean} {vol.Optional(CONF_FORCE, default=False): cv.boolean}
) )
PLATFORMS = ["sensor", "switch"]
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the AdGuard Home components."""
return True
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.""" """Set up AdGuard Home from a config entry."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
adguard = AdGuardHome( adguard = AdGuardHome(
@ -69,32 +68,36 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
except AdGuardHomeConnectionError as exception: except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception raise ConfigEntryNotReady from exception
for component in "sensor", "switch": for platform in PLATFORMS:
hass.async_create_task( 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: async def add_url(call) -> None:
"""Service call to add a new filter subscription to AdGuard Home.""" """Service call to add a new filter subscription to AdGuard Home."""
await adguard.filtering.add_url( 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: async def remove_url(call) -> None:
"""Service call to remove a filter subscription from AdGuard Home.""" """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: async def enable_url(call) -> None:
"""Service call to enable a filter subscription in AdGuard Home.""" """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: async def disable_url(call) -> None:
"""Service call to disable a filter subscription in AdGuard Home.""" """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: async def refresh(call) -> None:
"""Service call to refresh the filter subscriptions in AdGuard Home.""" """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( hass.services.async_register(
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA 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 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.""" """Unload AdGuard Home config entry."""
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_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_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH) hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
for component in "sensor", "switch": for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, component) await hass.config_entries.async_forward_entry_unload(entry, platform)
del hass.data[DOMAIN] del hass.data[DOMAIN]
@ -189,7 +192,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
"""Defines a AdGuard Home device entity.""" """Defines a AdGuard Home device entity."""
@property @property
def device_info(self) -> Dict[str, Any]: def device_info(self) -> dict[str, Any]:
"""Return device information about this AdGuard Home instance.""" """Return device information about this AdGuard Home instance."""
return { return {
"identifiers": { "identifiers": {

View File

@ -1,9 +1,12 @@
"""Config flow to configure the AdGuard Home integration.""" """Config flow to configure the AdGuard Home integration."""
from __future__ import annotations
from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.adguard.const import DOMAIN
from homeassistant.config_entries import ConfigFlow from homeassistant.config_entries import ConfigFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -15,9 +18,10 @@ from homeassistant.const import (
) )
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.""" """Handle a AdGuard Home config flow."""
VERSION = 1 VERSION = 1
@ -25,7 +29,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
_hassio_discovery = None _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.""" """Show the setup form to the user."""
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@ -42,7 +48,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
errors=errors or {}, 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.""" """Show the Hass.io confirmation form to the user."""
return self.async_show_form( return self.async_show_form(
step_id="hassio_confirm", step_id="hassio_confirm",
@ -51,7 +59,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
errors=errors or {}, 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.""" """Handle a flow initiated by the user."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") 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. """Prepare configuration for a Hass.io AdGuard Home add-on.
This flow is triggered by the discovery component. This flow is triggered by the discovery component.
@ -100,6 +110,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
if not entries: if not entries:
self._hassio_discovery = discovery_info self._hassio_discovery = discovery_info
await self._async_handle_discovery_without_unique_id()
return await self.async_step_hassio_confirm() return await self.async_step_hassio_confirm()
cur_entry = entries[0] cur_entry = entries[0]
@ -129,7 +140,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
return self.async_abort(reason="existing_instance_updated") 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.""" """Confirm Hass.io discovery."""
if user_input is None: if user_input is None:
return await self._show_hassio_form() return await self._show_hassio_form()

View File

@ -3,6 +3,6 @@
"name": "AdGuard Home", "name": "AdGuard Home",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adguard", "documentation": "https://www.home-assistant.io/integrations/adguard",
"requirements": ["adguardhome==0.4.2"], "requirements": ["adguardhome==0.5.0"],
"codeowners": ["@frenck"] "codeowners": ["@frenck"]
} }

View File

@ -1,25 +1,29 @@
"""Support for AdGuard Home sensors.""" """Support for AdGuard Home sensors."""
from __future__ import annotations
from datetime import timedelta 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.sensor import SensorEntity
from homeassistant.components.adguard.const import (
DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady 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) SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4 PARALLEL_UPDATES = 4
async def async_setup_entry( 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: ) -> None:
"""Set up AdGuard Home sensor based on a config entry.""" """Set up AdGuard Home sensor based on a config entry."""
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
@ -45,12 +49,12 @@ async def async_setup_entry(
async_add_entities(sensors, True) async_add_entities(sensors, True)
class AdGuardHomeSensor(AdGuardHomeDeviceEntity): class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity):
"""Defines a AdGuard Home sensor.""" """Defines a AdGuard Home sensor."""
def __init__( def __init__(
self, self,
adguard, adguard: AdGuardHome,
name: str, name: str,
icon: str, icon: str,
measurement: str, measurement: str,
@ -78,12 +82,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
) )
@property @property
def state(self): def state(self) -> str | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
@property @property
def unit_of_measurement(self) -> str: def unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
return self._unit_of_measurement return self._unit_of_measurement
@ -91,7 +95,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home DNS Queries sensor.""" """Defines a AdGuard Home DNS Queries sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries" adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries"
@ -105,7 +109,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home blocked by filtering sensor.""" """Defines a AdGuard Home blocked by filtering sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -124,7 +128,7 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home blocked percentage sensor.""" """Defines a AdGuard Home blocked percentage sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -143,7 +147,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home replaced by parental control sensor.""" """Defines a AdGuard Home replaced by parental control sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -161,7 +165,7 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home replaced by safe browsing sensor.""" """Defines a AdGuard Home replaced by safe browsing sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -179,7 +183,7 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home replaced by safe search sensor.""" """Defines a AdGuard Home replaced by safe search sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -197,7 +201,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home average processing time sensor.""" """Defines a AdGuard Home average processing time sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -216,7 +220,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
"""Defines a AdGuard Home rules count sensor.""" """Defines a AdGuard Home rules count sensor."""
def __init__(self, adguard): def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""
super().__init__( super().__init__(
adguard, adguard,
@ -229,4 +233,4 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
async def _adguard_update(self) -> None: async def _adguard_update(self) -> None:
"""Update AdGuard Home entity.""" """Update AdGuard Home entity."""
self._state = await self.adguard.filtering.rules_count() self._state = await self.adguard.filtering.rules_count(allowlist=False)

View File

@ -13,8 +13,8 @@
} }
}, },
"hassio_confirm": { "hassio_confirm": {
"title": "AdGuard Home via Hass.io add-on", "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 Hass.io add-on: {addon}?" "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?"
} }
}, },
"error": { "error": {

View File

@ -1,19 +1,20 @@
"""Support for AdGuard Home switches.""" """Support for AdGuard Home switches."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging 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.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady 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__) _LOGGER = logging.getLogger(__name__)
@ -22,7 +23,9 @@ PARALLEL_UPDATES = 1
async def async_setup_entry( 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: ) -> None:
"""Set up AdGuard Home switch based on a config entry.""" """Set up AdGuard Home switch based on a config entry."""
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT] adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
@ -49,8 +52,13 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
"""Defines a AdGuard Home switch.""" """Defines a AdGuard Home switch."""
def __init__( 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.""" """Initialize AdGuard Home switch."""
self._state = False self._state = False
self._key = key self._key = key
@ -96,7 +104,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home protection switch.""" """Defines a AdGuard Home protection switch."""
def __init__(self, adguard) -> None: def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__( super().__init__(
adguard, "AdGuard Protection", "mdi:shield-check", "protection" adguard, "AdGuard Protection", "mdi:shield-check", "protection"
@ -118,7 +126,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home parental control switch.""" """Defines a AdGuard Home parental control switch."""
def __init__(self, adguard) -> None: def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__( super().__init__(
adguard, "AdGuard Parental Control", "mdi:shield-check", "parental" adguard, "AdGuard Parental Control", "mdi:shield-check", "parental"
@ -140,7 +148,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home safe search switch.""" """Defines a AdGuard Home safe search switch."""
def __init__(self, adguard) -> None: def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__( super().__init__(
adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch" adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch"
@ -162,7 +170,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home safe search switch.""" """Defines a AdGuard Home safe search switch."""
def __init__(self, adguard) -> None: def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__( super().__init__(
adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing"
@ -184,7 +192,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home filtering switch.""" """Defines a AdGuard Home filtering switch."""
def __init__(self, adguard) -> None: def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering") super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering")
@ -204,7 +212,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch):
"""Defines a AdGuard Home query log switch.""" """Defines a AdGuard Home query log switch."""
def __init__(self, adguard) -> None: def __init__(self, adguard: AdGuardHome) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""
super().__init__( super().__init__(
adguard, adguard,

View File

@ -6,8 +6,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "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} ?", "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 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" "title": "AdGuard Home \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -10,7 +10,7 @@
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?", "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": { "user": {
"data": { "data": {

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?", "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed Supervisor {addon}?",
"title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Supervisor"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -6,8 +6,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?", "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Supervisor-tilf\u00f8jelsen: {addon}?",
"title": "AdGuard Home via Hass.io-tilf\u00f8jelse" "title": "AdGuard Home via Supervisor-tilf\u00f8jelse"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?", "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 Hass.io Add-on" "title": "AdGuard Home \u00fcber das Supervisor Add-on"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -6,8 +6,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Hass.io: {addon}?", "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 Hass.io" "title": "AdGuard Home a trav\u00e9s del complemento Supervisor"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?", "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?",
"title": "AdGuard Home a trav\u00e9s del complemento Hass.io" "title": "AdGuard Home a trav\u00e9s del complemento Supervisor"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -10,7 +10,7 @@
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub Hass.io lisandmoodul: {addon} ?", "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": { "user": {
"data": { "data": {

View File

@ -4,6 +4,7 @@
"user": { "user": {
"data": { "data": {
"host": "Host", "host": "Host",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
"port": "\u05e4\u05d5\u05e8\u05d8" "port": "\u05e4\u05d5\u05e8\u05d8"
} }
} }

View File

@ -1,5 +1,8 @@
{ {
"config": { "config": {
"abort": {
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
"error": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s" "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}, },
@ -9,7 +12,9 @@
"host": "Hoszt", "host": "Hoszt",
"password": "Jelsz\u00f3", "password": "Jelsz\u00f3",
"port": "Port", "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"
} }
} }
} }

View File

@ -1,10 +1,27 @@
{ {
"config": { "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": { "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": { "user": {
"data": { "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."
} }
} }
} }

View File

@ -9,7 +9,7 @@
}, },
"step": { "step": {
"hassio_confirm": { "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" "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io"
}, },
"user": { "user": {

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \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": { "error": {
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "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?", "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 Hass.io add-on" "title": "AdGuard Home via Supervisor add-on"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Hass.io-add-on: {addon}?", "description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Supervisor-add-on: {addon}?",
"title": "AdGuard Home via Hass.io add-on" "title": "AdGuard Home via Supervisor add-on"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -9,7 +9,7 @@
}, },
"step": { "step": {
"hassio_confirm": { "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" "title": "AdGuard Home via Hass.io-tillegg"
}, },
"user": { "user": {

View File

@ -9,7 +9,7 @@
}, },
"step": { "step": {
"hassio_confirm": { "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" "title": "AdGuard Home przez dodatek Hass.io"
}, },
"user": { "user": {

View File

@ -6,8 +6,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Hass.io: {addon} ?", "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Supervisor: {addon} ?",
"title": "AdGuard Home via add-on Hass.io" "title": "AdGuard Home via add-on Supervisor"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -8,7 +8,7 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"title": "AdGuard Home via Hass.io add-on" "title": "AdGuard Home via Supervisor add-on"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -18,7 +18,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c", "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442", "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", "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" "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." "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."

View File

@ -6,8 +6,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?", "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Supervisor add-on {addon} ?",
"title": "AdGuard Home preko dodatka Hass.io" "title": "AdGuard Home preko dodatka Supervisor"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -6,8 +6,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "hassio_confirm": {
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Hass.io Add-on: {addon}?", "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 Hass.io-till\u00e4gget" "title": "AdGuard Home via Supervisor-till\u00e4gget"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "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}\")?", "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 Hass.io)" "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -9,8 +9,8 @@
}, },
"step": { "step": {
"hassio_confirm": { "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", "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\u7d44\u4ef6 AdGuard Home" "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 AdGuard Home"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -2,7 +2,7 @@
import voluptuous as vol import voluptuous as vol
from homeassistant.components import ads 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 from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities([entity]) add_entities([entity])
class AdsSensor(AdsEntity): class AdsSensor(AdsEntity, SensorEntity):
"""Representation of an ADS sensor entity.""" """Representation of an ADS sensor entity."""
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):

View File

@ -7,24 +7,17 @@ import logging
from advantage_air import ApiError, advantage_air from advantage_air import ApiError, advantage_air
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT 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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY, DOMAIN from .const import ADVANTAGE_AIR_RETRY, DOMAIN
ADVANTAGE_AIR_SYNC_INTERVAL = 15 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__) _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): async def async_setup_entry(hass, entry):
"""Set up Advantage Air config.""" """Set up Advantage Air config."""
ip_address = entry.data[CONF_IP_ADDRESS] ip_address = entry.data[CONF_IP_ADDRESS]
@ -57,17 +50,15 @@ async def async_setup_entry(hass, entry):
except ApiError as err: except ApiError as err:
_LOGGER.warning(err) _LOGGER.warning(err)
await coordinator.async_refresh() await coordinator.async_config_entry_first_refresh()
if not coordinator.data:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
"coordinator": coordinator, "coordinator": coordinator,
"async_change": async_change, "async_change": async_change,
} }
for platform in ADVANTAGE_AIR_PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform) hass.config_entries.async_forward_entry_setup(entry, platform)
) )
@ -80,8 +71,8 @@ async def async_unload_entry(hass, entry):
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_unload(entry, component) hass.config_entries.async_forward_entry_unload(entry, platform)
for component in ADVANTAGE_AIR_PLATFORMS for platform in PLATFORMS
] ]
) )
) )

View File

@ -1,6 +1,7 @@
"""Sensor platform for Advantage Air integration.""" """Sensor platform for Advantage Air integration."""
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import PERCENTAGE from homeassistant.const import PERCENTAGE
from homeassistant.helpers import config_validation as cv, entity_platform 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.""" """Representation of Advantage Air timer control."""
def __init__(self, instance, ac_key, action): 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}}}) 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.""" """Representation of Advantage Air Zone Vent Sensor."""
@property @property
@ -115,7 +116,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity):
return "mdi:fan-off" return "mdi:fan-off"
class AdvantageAirZoneSignal(AdvantageAirEntity): class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor.""" """Representation of Advantage Air Zone wireless signal sensor."""
@property @property

View File

@ -1,5 +1,8 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
"error": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s" "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}, },

View File

@ -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"
}
}
}
}

View File

@ -11,7 +11,9 @@
"data": { "data": {
"ip_address": "IP \uc8fc\uc18c", "ip_address": "IP \uc8fc\uc18c",
"port": "\ud3ec\ud2b8" "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"
} }
} }
} }

View File

@ -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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant 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 from .weather_update_coordinator import WeatherUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _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): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Set up AEMET OpenData as config entry.""" """Set up AEMET OpenData as config entry."""
name = config_entry.data[CONF_NAME] name = config_entry.data[CONF_NAME]
@ -30,16 +24,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
aemet = AEMET(api_key) aemet = AEMET(api_key)
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude) 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] = { hass.data[DOMAIN][config_entry.entry_id] = {
ENTRY_NAME: name, ENTRY_NAME: name,
ENTRY_WEATHER_COORDINATOR: weather_coordinator, ENTRY_WEATHER_COORDINATOR: weather_coordinator,
} }
for component in COMPONENTS: for platform in PLATFORMS:
hass.async_create_task( 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 return True
@ -50,8 +45,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_unload(config_entry, component) hass.config_entries.async_forward_entry_unload(config_entry, platform)
for component in COMPONENTS for platform in PLATFORMS
] ]
) )
) )

View File

@ -1,4 +1,5 @@
"""Abstraction form AEMET OpenData sensors.""" """Abstraction form AEMET OpenData sensors."""
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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 from .weather_update_coordinator import WeatherUpdateCoordinator
class AbstractAemetSensor(CoordinatorEntity): class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
"""Abstract class for an AEMET OpenData sensor.""" """Abstract class for an AEMET OpenData sensor."""
def __init__( def __init__(
@ -52,6 +53,6 @@ class AbstractAemetSensor(CoordinatorEntity):
return self._unit_of_measurement return self._unit_of_measurement
@property @property
def device_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION} return {ATTR_ATTRIBUTION: ATTRIBUTION}

View File

@ -6,8 +6,7 @@ from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import DEFAULT_NAME from .const import DEFAULT_NAME, DOMAIN
from .const import DOMAIN # pylint:disable=unused-import
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):

View File

@ -34,7 +34,7 @@ from homeassistant.const import (
) )
ATTRIBUTION = "Powered by AEMET OpenData" ATTRIBUTION = "Powered by AEMET OpenData"
COMPONENTS = ["sensor", "weather"] PLATFORMS = ["sensor", "weather"]
DEFAULT_NAME = "AEMET" DEFAULT_NAME = "AEMET"
DOMAIN = "aemet" DOMAIN = "aemet"
ENTRY_NAME = "name" ENTRY_NAME = "name"

View File

@ -106,9 +106,10 @@ class AemetForecastSensor(AbstractAemetSensor):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
forecast = None
forecasts = self._weather_coordinator.data.get( forecasts = self._weather_coordinator.data.get(
FORECAST_MODE_ATTR_API[self._forecast_mode] FORECAST_MODE_ATTR_API[self._forecast_mode]
) )
if forecasts: if forecasts:
return forecasts[0].get(self._sensor_type) forecast = forecasts[0].get(self._sensor_type)
return None return forecast

View File

@ -11,8 +11,11 @@
"data": { "data": {
"api_key": "API-Schl\u00fcssel", "api_key": "API-Schl\u00fcssel",
"latitude": "Breitengrad", "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]"
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More