mirror of
https://github.com/home-assistant/core.git
synced 2025-08-01 09:38:21 +00:00
Merge pull request #48782 from home-assistant/rc
This commit is contained in:
commit
8bdcdfb8e6
61
.coveragerc
61
.coveragerc
@ -221,6 +221,7 @@ omit =
|
||||
homeassistant/components/ecobee/weather.py
|
||||
homeassistant/components/econet/__init__.py
|
||||
homeassistant/components/econet/binary_sensor.py
|
||||
homeassistant/components/econet/climate.py
|
||||
homeassistant/components/econet/const.py
|
||||
homeassistant/components/econet/sensor.py
|
||||
homeassistant/components/econet/water_heater.py
|
||||
@ -314,7 +315,6 @@ omit =
|
||||
homeassistant/components/foscam/camera.py
|
||||
homeassistant/components/foursquare/*
|
||||
homeassistant/components/free_mobile/notify.py
|
||||
homeassistant/components/freebox/__init__.py
|
||||
homeassistant/components/freebox/device_tracker.py
|
||||
homeassistant/components/freebox/router.py
|
||||
homeassistant/components/freebox/sensor.py
|
||||
@ -377,6 +377,9 @@ omit =
|
||||
homeassistant/components/harmony/data.py
|
||||
homeassistant/components/harmony/remote.py
|
||||
homeassistant/components/harmony/util.py
|
||||
homeassistant/components/hassio/binary_sensor.py
|
||||
homeassistant/components/hassio/entity.py
|
||||
homeassistant/components/hassio/sensor.py
|
||||
homeassistant/components/haveibeenpwned/sensor.py
|
||||
homeassistant/components/hdmi_cec/*
|
||||
homeassistant/components/heatmiser/climate.py
|
||||
@ -384,7 +387,13 @@ omit =
|
||||
homeassistant/components/hikvisioncam/switch.py
|
||||
homeassistant/components/hisense_aehw4a1/*
|
||||
homeassistant/components/hitron_coda/device_tracker.py
|
||||
homeassistant/components/hive/*
|
||||
homeassistant/components/hive/__init__.py
|
||||
homeassistant/components/hive/climate.py
|
||||
homeassistant/components/hive/binary_sensor.py
|
||||
homeassistant/components/hive/light.py
|
||||
homeassistant/components/hive/sensor.py
|
||||
homeassistant/components/hive/switch.py
|
||||
homeassistant/components/hive/water_heater.py
|
||||
homeassistant/components/hlk_sw16/__init__.py
|
||||
homeassistant/components/hlk_sw16/switch.py
|
||||
homeassistant/components/home_connect/*
|
||||
@ -392,6 +401,9 @@ omit =
|
||||
homeassistant/components/homematic/climate.py
|
||||
homeassistant/components/homematic/cover.py
|
||||
homeassistant/components/homematic/notify.py
|
||||
homeassistant/components/home_plus_control/api.py
|
||||
homeassistant/components/home_plus_control/helpers.py
|
||||
homeassistant/components/home_plus_control/switch.py
|
||||
homeassistant/components/homeworks/*
|
||||
homeassistant/components/honeywell/climate.py
|
||||
homeassistant/components/horizon/media_player.py
|
||||
@ -497,7 +509,18 @@ omit =
|
||||
homeassistant/components/lastfm/sensor.py
|
||||
homeassistant/components/launch_library/const.py
|
||||
homeassistant/components/launch_library/sensor.py
|
||||
homeassistant/components/lcn/*
|
||||
homeassistant/components/lcn/__init__.py
|
||||
homeassistant/components/lcn/binary_sensor.py
|
||||
homeassistant/components/lcn/climate.py
|
||||
homeassistant/components/lcn/const.py
|
||||
homeassistant/components/lcn/cover.py
|
||||
homeassistant/components/lcn/helpers.py
|
||||
homeassistant/components/lcn/light.py
|
||||
homeassistant/components/lcn/scene.py
|
||||
homeassistant/components/lcn/schemas.py
|
||||
homeassistant/components/lcn/sensor.py
|
||||
homeassistant/components/lcn/services.py
|
||||
homeassistant/components/lcn/switch.py
|
||||
homeassistant/components/lg_netcast/media_player.py
|
||||
homeassistant/components/lg_soundbar/media_player.py
|
||||
homeassistant/components/life360/*
|
||||
@ -540,7 +563,6 @@ omit =
|
||||
homeassistant/components/map/*
|
||||
homeassistant/components/mastodon/notify.py
|
||||
homeassistant/components/matrix/*
|
||||
homeassistant/components/maxcube/*
|
||||
homeassistant/components/mcp23017/*
|
||||
homeassistant/components/media_extractor/*
|
||||
homeassistant/components/mediaroom/media_player.py
|
||||
@ -624,17 +646,6 @@ omit =
|
||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||
homeassistant/components/nello/lock.py
|
||||
homeassistant/components/nest/legacy/*
|
||||
homeassistant/components/netatmo/__init__.py
|
||||
homeassistant/components/netatmo/api.py
|
||||
homeassistant/components/netatmo/camera.py
|
||||
homeassistant/components/netatmo/climate.py
|
||||
homeassistant/components/netatmo/const.py
|
||||
homeassistant/components/netatmo/data_handler.py
|
||||
homeassistant/components/netatmo/helper.py
|
||||
homeassistant/components/netatmo/light.py
|
||||
homeassistant/components/netatmo/netatmo_entity_base.py
|
||||
homeassistant/components/netatmo/sensor.py
|
||||
homeassistant/components/netatmo/webhook.py
|
||||
homeassistant/components/netdata/sensor.py
|
||||
homeassistant/components/netgear/device_tracker.py
|
||||
homeassistant/components/netgear_lte/*
|
||||
@ -723,12 +734,14 @@ omit =
|
||||
homeassistant/components/pencom/switch.py
|
||||
homeassistant/components/philips_js/__init__.py
|
||||
homeassistant/components/philips_js/media_player.py
|
||||
homeassistant/components/philips_js/remote.py
|
||||
homeassistant/components/pi_hole/sensor.py
|
||||
homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py
|
||||
homeassistant/components/pi4ioe5v9xxxx/switch.py
|
||||
homeassistant/components/picotts/tts.py
|
||||
homeassistant/components/piglow/light.py
|
||||
homeassistant/components/pilight/*
|
||||
homeassistant/components/ping/__init__.py
|
||||
homeassistant/components/ping/const.py
|
||||
homeassistant/components/ping/binary_sensor.py
|
||||
homeassistant/components/ping/device_tracker.py
|
||||
@ -782,6 +795,7 @@ omit =
|
||||
homeassistant/components/raspyrfm/*
|
||||
homeassistant/components/recollect_waste/__init__.py
|
||||
homeassistant/components/recollect_waste/sensor.py
|
||||
homeassistant/components/recorder/repack.py
|
||||
homeassistant/components/recswitch/switch.py
|
||||
homeassistant/components/reddit/*
|
||||
homeassistant/components/rejseplanen/sensor.py
|
||||
@ -823,6 +837,11 @@ omit =
|
||||
homeassistant/components/satel_integra/*
|
||||
homeassistant/components/schluter/*
|
||||
homeassistant/components/scrape/sensor.py
|
||||
homeassistant/components/screenlogic/__init__.py
|
||||
homeassistant/components/screenlogic/binary_sensor.py
|
||||
homeassistant/components/screenlogic/climate.py
|
||||
homeassistant/components/screenlogic/sensor.py
|
||||
homeassistant/components/screenlogic/switch.py
|
||||
homeassistant/components/scsgate/*
|
||||
homeassistant/components/scsgate/cover.py
|
||||
homeassistant/components/sendgrid/notify.py
|
||||
@ -1045,7 +1064,14 @@ omit =
|
||||
homeassistant/components/velbus/switch.py
|
||||
homeassistant/components/velux/*
|
||||
homeassistant/components/venstar/climate.py
|
||||
homeassistant/components/verisure/*
|
||||
homeassistant/components/verisure/__init__.py
|
||||
homeassistant/components/verisure/alarm_control_panel.py
|
||||
homeassistant/components/verisure/binary_sensor.py
|
||||
homeassistant/components/verisure/camera.py
|
||||
homeassistant/components/verisure/coordinator.py
|
||||
homeassistant/components/verisure/lock.py
|
||||
homeassistant/components/verisure/sensor.py
|
||||
homeassistant/components/verisure/switch.py
|
||||
homeassistant/components/versasense/*
|
||||
homeassistant/components/vesync/__init__.py
|
||||
homeassistant/components/vesync/common.py
|
||||
@ -1164,3 +1190,6 @@ exclude_lines =
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# TYPE_CHECKING block is never executed during pytest run
|
||||
if TYPE_CHECKING:
|
||||
|
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
custom: https://www.nabucasa.com
|
||||
github: balloob
|
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
17
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,5 +1,5 @@
|
||||
name: Report an issue with Home Assistant Core
|
||||
about: Report an issue with Home Assistant Core.
|
||||
description: Report an issue with Home Assistant Core.
|
||||
title: ""
|
||||
issue_body: true
|
||||
body:
|
||||
@ -26,6 +26,7 @@ body:
|
||||
value: |
|
||||
## Environment
|
||||
- type: input
|
||||
id: version
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
@ -52,11 +53,13 @@ body:
|
||||
- Home Assistant Supervised
|
||||
- Home Assistant Core
|
||||
- type: input
|
||||
id: integration_name
|
||||
attributes:
|
||||
label: Integration causing the issue
|
||||
description: >
|
||||
The name of the integration, for example, Automation or Philips Hue.
|
||||
- type: input
|
||||
id: integration_link
|
||||
attributes:
|
||||
label: Link to integration documentation on our website
|
||||
placeholder: "https://www.home-assistant.io/integrations/..."
|
||||
@ -76,20 +79,12 @@ body:
|
||||
description: |
|
||||
If this issue has an example piece of YAML that can help reproducing this problem, please provide.
|
||||
This can be an piece of YAML from, e.g., an automation, script, scene or configuration.
|
||||
value: |
|
||||
```yaml
|
||||
# Put your YAML below this line
|
||||
|
||||
```
|
||||
render: yaml
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything in the logs that might be useful for us?
|
||||
description: For example, error message, or stack traces.
|
||||
value: |
|
||||
```txt
|
||||
# Put your logs below this line
|
||||
|
||||
```
|
||||
render: txt
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
279
.github/workflows/ci.yaml
vendored
279
.github/workflows/ci.yaml
vendored
@ -12,7 +12,7 @@ on:
|
||||
env:
|
||||
CACHE_VERSION: 1
|
||||
DEFAULT_PYTHON: 3.8
|
||||
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
||||
|
||||
jobs:
|
||||
# Separate job to pre-populate the base dependency cache
|
||||
@ -20,6 +20,9 @@ jobs:
|
||||
prepare-base:
|
||||
name: Prepare base dependencies
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python-key: ${{ steps.generate-python-key.outputs.key }}
|
||||
pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
@ -28,21 +31,25 @@ jobs:
|
||||
uses: actions/setup-python@v2.2.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate-python-key
|
||||
run: >-
|
||||
echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}"
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
steps.generate-python-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -50,15 +57,20 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pip install -U "pip<20.3" setuptools
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
- name: Generate partial pre-commit restore key
|
||||
id: generate-pre-commit-key
|
||||
run: >-
|
||||
echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{
|
||||
hashFiles('.pre-commit-config.yaml') }}"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-
|
||||
${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}-
|
||||
- name: Install pre-commit dependencies
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -82,12 +94,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -97,13 +105,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Run bandit
|
||||
run: |
|
||||
@ -127,12 +134,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -142,13 +145,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Run black
|
||||
run: |
|
||||
@ -172,12 +174,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -187,13 +185,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Register codespell problem matcher
|
||||
run: |
|
||||
@ -239,12 +236,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -254,13 +247,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Register check executables problem matcher
|
||||
run: |
|
||||
@ -287,12 +279,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -302,13 +290,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Register flake8 problem matcher
|
||||
run: |
|
||||
@ -335,12 +322,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -350,13 +333,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Run isort
|
||||
run: |
|
||||
@ -380,12 +362,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -395,13 +373,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Register check-json problem matcher
|
||||
run: |
|
||||
@ -428,12 +405,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -443,13 +416,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Run pyupgrade
|
||||
run: |
|
||||
@ -484,12 +456,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -499,13 +467,12 @@ jobs:
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Fail job if cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Register yamllint problem matcher
|
||||
run: |
|
||||
@ -531,11 +498,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
|
||||
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
|
||||
}}-${{ hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.prepare-tests.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -563,12 +527,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
|
||||
steps.python.outputs.python-version }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.prepare-base.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -585,24 +545,31 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8, 3.9]
|
||||
outputs:
|
||||
python-key: ${{ steps.generate-python-key.outputs.key }}
|
||||
container: homeassistant/ci-azure:${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate-python-key
|
||||
run: >-
|
||||
echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}"
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
|
||||
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
|
||||
}}-${{ hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
steps.generate-python-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-
|
||||
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}-
|
||||
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-
|
||||
${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-
|
||||
- name: Create full Python ${{ matrix.python-version }} virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -633,11 +600,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
|
||||
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
|
||||
}}-${{ hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.prepare-tests.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -667,11 +631,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
|
||||
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
|
||||
}}-${{ hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.prepare-tests.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -689,6 +650,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-tests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
group: [1, 2, 3, 4]
|
||||
python-version: [3.8, 3.9]
|
||||
@ -703,11 +665,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
|
||||
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
|
||||
}}-${{ hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.prepare-tests.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -735,6 +694,7 @@ jobs:
|
||||
--test-group-count 4 \
|
||||
--test-group=${{ matrix.group }} \
|
||||
--cov homeassistant \
|
||||
--cov-report= \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
tests
|
||||
@ -750,7 +710,7 @@ jobs:
|
||||
coverage:
|
||||
name: Process test coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: pytest
|
||||
needs: ["prepare-tests", "pytest"]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
@ -763,11 +723,8 @@ jobs:
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
|
||||
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
|
||||
}}-${{ hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}
|
||||
key: ${{ runner.os }}-${{ matrix.python-version }}-${{
|
||||
needs.prepare-tests.outputs.python-key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -782,4 +739,4 @@ jobs:
|
||||
coverage report --fail-under=94
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.2.1
|
||||
uses: codecov/codecov-action@v1.3.1
|
||||
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
# - No PRs marked as no-stale
|
||||
# - No issues marked as no-stale or help-wanted
|
||||
- name: 90 days stale issues & PRs policy
|
||||
uses: actions/stale@v3.0.17
|
||||
uses: actions/stale@v3.0.18
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
# - No PRs marked as no-stale or new-integrations
|
||||
# - No issues (-1)
|
||||
- name: 30 days stale PRs policy
|
||||
uses: actions/stale@v3.0.17
|
||||
uses: actions/stale@v3.0.18
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
# - No Issues marked as no-stale or help-wanted
|
||||
# - No PRs (-1)
|
||||
- name: Needs more information stale issues policy
|
||||
uses: actions/stale@v3.0.17
|
||||
uses: actions/stale@v3.0.18
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-information"
|
||||
|
@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.7.2
|
||||
rev: v2.11.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
@ -23,12 +23,16 @@ repos:
|
||||
exclude_types: [csv, json]
|
||||
exclude: ^tests/fixtures/
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.4
|
||||
rev: 3.9.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.5.0
|
||||
- pydocstyle==5.1.1
|
||||
- pycodestyle==2.7.0
|
||||
- pyflakes==2.3.1
|
||||
- flake8-docstrings==1.6.0
|
||||
- pydocstyle==6.0.0
|
||||
- flake8-comprehensions==3.4.0
|
||||
- flake8-noqa==1.1.0
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.0
|
||||
@ -40,7 +44,7 @@ repos:
|
||||
- --configfile=tests/bandit.yaml
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.5.3
|
||||
rev: 5.7.0
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
@ -64,6 +68,19 @@ repos:
|
||||
hooks:
|
||||
- id: prettier
|
||||
stages: [manual]
|
||||
- repo: https://github.com/cdce8p/python-typing-update
|
||||
rev: v0.3.2
|
||||
hooks:
|
||||
# Run `python-typing-update` hook manually from time to time
|
||||
# to update python typing syntax.
|
||||
# Will require manual work, before submitting changes!
|
||||
- id: python-typing-update
|
||||
stages: [manual]
|
||||
args:
|
||||
- --py38-plus
|
||||
- --force
|
||||
- --keep-updates
|
||||
files: ^(homeassistant|tests|script)/.+\.py$
|
||||
- repo: local
|
||||
hooks:
|
||||
# Run mypy through our wrapper script in order to get the possible
|
||||
|
14
CODEOWNERS
14
CODEOWNERS
@ -36,6 +36,7 @@ homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/amcrest/* @pnbruckner
|
||||
homeassistant/components/analytics/* @home-assistant/core @ludeeus
|
||||
homeassistant/components/androidtv/* @JeffLIrion
|
||||
homeassistant/components/apache_kafka/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
@ -180,14 +181,12 @@ homeassistant/components/google_cloud/* @lufton
|
||||
homeassistant/components/gpsd/* @fabaff
|
||||
homeassistant/components/gree/* @cmroche
|
||||
homeassistant/components/greeneye_monitor/* @jkeljo
|
||||
homeassistant/components/griddy/* @bdraco
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
homeassistant/components/growatt_server/* @indykoning
|
||||
homeassistant/components/guardian/* @bachya
|
||||
homeassistant/components/habitica/* @ASMfreaK @leikoilja
|
||||
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
|
||||
homeassistant/components/hassio/* @home-assistant/supervisor
|
||||
homeassistant/components/hdmi_cec/* @newAM
|
||||
homeassistant/components/heatmiser/* @andylockran
|
||||
homeassistant/components/heos/* @andrewsayre
|
||||
homeassistant/components/here_travel_time/* @eifinger
|
||||
@ -198,11 +197,11 @@ homeassistant/components/history/* @home-assistant/core
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/hlk_sw16/* @jameshilliard
|
||||
homeassistant/components/home_connect/* @DavidMStraub
|
||||
homeassistant/components/home_plus_control/* @chemaaa
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @bdraco
|
||||
homeassistant/components/homekit_controller/* @Jc2k
|
||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||
homeassistant/components/homematicip_cloud/* @SukramJ
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/huawei_lte/* @scop @fphammerle
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
@ -403,6 +402,7 @@ homeassistant/components/samsungtv/* @escoand
|
||||
homeassistant/components/scene/* @home-assistant/core
|
||||
homeassistant/components/schluter/* @prairieapps
|
||||
homeassistant/components/scrape/* @fabaff
|
||||
homeassistant/components/screenlogic/* @dieselrabbit
|
||||
homeassistant/components/script/* @home-assistant/core
|
||||
homeassistant/components/search/* @home-assistant/core
|
||||
homeassistant/components/sense/* @kbickar
|
||||
@ -433,12 +433,12 @@ homeassistant/components/smarttub/* @mdz
|
||||
homeassistant/components/smarty/* @z0mbieprocess
|
||||
homeassistant/components/sms/* @ocalvo
|
||||
homeassistant/components/smtp/* @fabaff
|
||||
homeassistant/components/solaredge/* @frenck
|
||||
homeassistant/components/solaredge_local/* @drobtravels @scheric
|
||||
homeassistant/components/solarlog/* @Ernst79
|
||||
homeassistant/components/solax/* @squishykid
|
||||
homeassistant/components/soma/* @ratsept
|
||||
homeassistant/components/somfy/* @tetienne
|
||||
homeassistant/components/somfy_mylink/* @bdraco
|
||||
homeassistant/components/sonarr/* @ctalkington
|
||||
homeassistant/components/songpal/* @rytilahti @shenxn
|
||||
homeassistant/components/sonos/* @cgtobi
|
||||
@ -454,7 +454,7 @@ homeassistant/components/starline/* @anonym-tsk
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/stookalert/* @fwestenberg
|
||||
homeassistant/components/stream/* @hunterjm @uvjustin
|
||||
homeassistant/components/stream/* @hunterjm @uvjustin @allenporter
|
||||
homeassistant/components/stt/* @pvizeli
|
||||
homeassistant/components/subaru/* @G-Two
|
||||
homeassistant/components/suez_water/* @ooii
|
||||
@ -492,6 +492,7 @@ homeassistant/components/toon/* @frenck
|
||||
homeassistant/components/totalconnect/* @austinmroczek
|
||||
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
|
||||
homeassistant/components/traccar/* @ludeeus
|
||||
homeassistant/components/trace/* @home-assistant/core
|
||||
homeassistant/components/trafikverket_train/* @endor-force
|
||||
homeassistant/components/trafikverket_weatherstation/* @endor-force
|
||||
homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
@ -512,7 +513,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/velbus/* @Cereal2nd @brefra
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/vera/* @vangorra
|
||||
homeassistant/components/vera/* @pavoni
|
||||
homeassistant/components/verisure/* @frenck
|
||||
homeassistant/components/versasense/* @flamm3blemuff1n
|
||||
homeassistant/components/version/* @fabaff @ludeeus
|
||||
@ -524,6 +525,7 @@ homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/vlc_telnet/* @rodripf @dmcc
|
||||
homeassistant/components/volkszaehler/* @fabaff
|
||||
homeassistant/components/volumio/* @OnFreund
|
||||
homeassistant/components/wake_on_lan/* @ntilley905
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
|
@ -28,10 +28,11 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Install Python dependencies from requirements
|
||||
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||
COPY requirements.txt requirements_test.txt requirements_test_pre_commit.txt ./
|
||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||
RUN pip3 install -r requirements_test.txt \
|
||||
&& rm -rf requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||
RUN pip3 install -r requirements.txt \
|
||||
&& pip3 install -r requirements_test.txt \
|
||||
&& rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""Start Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
@ -206,7 +207,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def cmdline() -> List[str]:
|
||||
def cmdline() -> list[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if os.path.basename(sys.argv[0]) == "__main__.py":
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
from typing import Any, Dict, Optional, Tuple, cast
|
||||
|
||||
import jwt
|
||||
|
||||
@ -36,8 +36,8 @@ class InvalidProvider(Exception):
|
||||
|
||||
async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[Dict[str, Any]],
|
||||
module_configs: List[Dict[str, Any]],
|
||||
provider_configs: list[dict[str, Any]],
|
||||
module_configs: list[dict[str, Any]],
|
||||
) -> AuthManager:
|
||||
"""Initialize an auth manager from config.
|
||||
|
||||
@ -87,8 +87,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
self,
|
||||
handler_key: Any,
|
||||
*,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
context: dict[str, Any] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> data_entry_flow.FlowHandler:
|
||||
"""Create a login flow."""
|
||||
auth_provider = self.auth_manager.get_auth_provider(*handler_key)
|
||||
@ -97,8 +97,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager):
|
||||
return await auth_provider.async_login_flow(context)
|
||||
|
||||
async def async_finish_flow(
|
||||
self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
self, flow: data_entry_flow.FlowHandler, result: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Return a user as result of login flow."""
|
||||
flow = cast(LoginFlow, flow)
|
||||
|
||||
@ -157,22 +157,22 @@ class AuthManager:
|
||||
self.login_flow = AuthManagerFlowManager(hass, self)
|
||||
|
||||
@property
|
||||
def auth_providers(self) -> List[AuthProvider]:
|
||||
def auth_providers(self) -> list[AuthProvider]:
|
||||
"""Return a list of available auth providers."""
|
||||
return list(self._providers.values())
|
||||
|
||||
@property
|
||||
def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
|
||||
def auth_mfa_modules(self) -> list[MultiFactorAuthModule]:
|
||||
"""Return a list of available auth modules."""
|
||||
return list(self._mfa_modules.values())
|
||||
|
||||
def get_auth_provider(
|
||||
self, provider_type: str, provider_id: Optional[str]
|
||||
) -> Optional[AuthProvider]:
|
||||
self, provider_type: str, provider_id: str | None
|
||||
) -> AuthProvider | None:
|
||||
"""Return an auth provider, None if not found."""
|
||||
return self._providers.get((provider_type, provider_id))
|
||||
|
||||
def get_auth_providers(self, provider_type: str) -> List[AuthProvider]:
|
||||
def get_auth_providers(self, provider_type: str) -> list[AuthProvider]:
|
||||
"""Return a List of auth provider of one type, Empty if not found."""
|
||||
return [
|
||||
provider
|
||||
@ -180,30 +180,30 @@ class AuthManager:
|
||||
if p_type == provider_type
|
||||
]
|
||||
|
||||
def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
|
||||
def get_auth_mfa_module(self, module_id: str) -> MultiFactorAuthModule | None:
|
||||
"""Return a multi-factor auth module, None if not found."""
|
||||
return self._mfa_modules.get(module_id)
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
async def async_get_users(self) -> list[models.User]:
|
||||
"""Retrieve all users."""
|
||||
return await self._store.async_get_users()
|
||||
|
||||
async def async_get_user(self, user_id: str) -> Optional[models.User]:
|
||||
async def async_get_user(self, user_id: str) -> models.User | None:
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_get_owner(self) -> Optional[models.User]:
|
||||
async def async_get_owner(self) -> models.User | None:
|
||||
"""Retrieve the owner."""
|
||||
users = await self.async_get_users()
|
||||
return next((user for user in users if user.is_owner), None)
|
||||
|
||||
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
|
||||
async def async_get_group(self, group_id: str) -> models.Group | None:
|
||||
"""Retrieve all groups."""
|
||||
return await self._store.async_get_group(group_id)
|
||||
|
||||
async def async_get_user_by_credentials(
|
||||
self, credentials: models.Credentials
|
||||
) -> Optional[models.User]:
|
||||
) -> models.User | None:
|
||||
"""Get a user by credential, return None if not found."""
|
||||
for user in await self.async_get_users():
|
||||
for creds in user.credentials:
|
||||
@ -213,7 +213,7 @@ class AuthManager:
|
||||
return None
|
||||
|
||||
async def async_create_system_user(
|
||||
self, name: str, group_ids: Optional[List[str]] = None
|
||||
self, name: str, group_ids: list[str] | None = None
|
||||
) -> models.User:
|
||||
"""Create a system user."""
|
||||
user = await self._store.async_create_user(
|
||||
@ -225,10 +225,10 @@ class AuthManager:
|
||||
return user
|
||||
|
||||
async def async_create_user(
|
||||
self, name: str, group_ids: Optional[List[str]] = None
|
||||
self, name: str, group_ids: list[str] | None = None
|
||||
) -> models.User:
|
||||
"""Create a user."""
|
||||
kwargs: Dict[str, Any] = {
|
||||
kwargs: dict[str, Any] = {
|
||||
"name": name,
|
||||
"is_active": True,
|
||||
"group_ids": group_ids or [],
|
||||
@ -294,12 +294,12 @@ class AuthManager:
|
||||
async def async_update_user(
|
||||
self,
|
||||
user: models.User,
|
||||
name: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
group_ids: Optional[List[str]] = None,
|
||||
name: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
group_ids: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Update a user."""
|
||||
kwargs: Dict[str, Any] = {}
|
||||
kwargs: dict[str, Any] = {}
|
||||
if name is not None:
|
||||
kwargs["name"] = name
|
||||
if group_ids is not None:
|
||||
@ -362,9 +362,9 @@ class AuthManager:
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> dict[str, str]:
|
||||
"""List enabled mfa modules for user."""
|
||||
modules: Dict[str, str] = OrderedDict()
|
||||
modules: dict[str, str] = OrderedDict()
|
||||
for module_id, module in self._mfa_modules.items():
|
||||
if await module.async_is_user_setup(user.id):
|
||||
modules[module_id] = module.name
|
||||
@ -373,12 +373,12 @@ class AuthManager:
|
||||
async def async_create_refresh_token(
|
||||
self,
|
||||
user: models.User,
|
||||
client_id: Optional[str] = None,
|
||||
client_name: Optional[str] = None,
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: Optional[str] = None,
|
||||
client_id: str | None = None,
|
||||
client_name: str | None = None,
|
||||
client_icon: str | None = None,
|
||||
token_type: str | None = None,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
credential: Optional[models.Credentials] = None,
|
||||
credential: models.Credentials | None = None,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
@ -432,13 +432,13 @@ class AuthManager:
|
||||
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Get refresh token by id."""
|
||||
return await self._store.async_get_refresh_token(token_id)
|
||||
|
||||
async def async_get_refresh_token_by_token(
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token_by_token(token)
|
||||
|
||||
@ -450,7 +450,7 @@ class AuthManager:
|
||||
|
||||
@callback
|
||||
def async_create_access_token(
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
||||
) -> str:
|
||||
"""Create a new access token."""
|
||||
self.async_validate_refresh_token(refresh_token, remote_ip)
|
||||
@ -471,7 +471,7 @@ class AuthManager:
|
||||
@callback
|
||||
def _async_resolve_provider(
|
||||
self, refresh_token: models.RefreshToken
|
||||
) -> Optional[AuthProvider]:
|
||||
) -> AuthProvider | None:
|
||||
"""Get the auth provider for the given refresh token.
|
||||
|
||||
Raises an exception if the expected provider is no longer available or return
|
||||
@ -492,7 +492,7 @@ class AuthManager:
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
||||
) -> None:
|
||||
"""Validate that a refresh token is usable.
|
||||
|
||||
@ -504,7 +504,7 @@ class AuthManager:
|
||||
|
||||
async def async_validate_access_token(
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Return refresh token if an access token is valid."""
|
||||
try:
|
||||
unverif_claims = jwt.decode(token, verify=False)
|
||||
@ -535,7 +535,7 @@ class AuthManager:
|
||||
@callback
|
||||
def _async_get_auth_provider(
|
||||
self, credentials: models.Credentials
|
||||
) -> Optional[AuthProvider]:
|
||||
) -> AuthProvider | None:
|
||||
"""Get auth provider from a set of credentials."""
|
||||
auth_provider_key = (
|
||||
credentials.auth_provider_type,
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Storage for auth models."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import hmac
|
||||
from logging import getLogger
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@ -34,15 +36,15 @@ class AuthStore:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users: Optional[Dict[str, models.User]] = None
|
||||
self._groups: Optional[Dict[str, models.Group]] = None
|
||||
self._perm_lookup: Optional[PermissionLookup] = None
|
||||
self._users: dict[str, models.User] | None = None
|
||||
self._groups: dict[str, models.Group] | None = None
|
||||
self._perm_lookup: PermissionLookup | None = None
|
||||
self._store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True
|
||||
)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def async_get_groups(self) -> List[models.Group]:
|
||||
async def async_get_groups(self) -> list[models.Group]:
|
||||
"""Retrieve all users."""
|
||||
if self._groups is None:
|
||||
await self._async_load()
|
||||
@ -50,7 +52,7 @@ class AuthStore:
|
||||
|
||||
return list(self._groups.values())
|
||||
|
||||
async def async_get_group(self, group_id: str) -> Optional[models.Group]:
|
||||
async def async_get_group(self, group_id: str) -> models.Group | None:
|
||||
"""Retrieve all users."""
|
||||
if self._groups is None:
|
||||
await self._async_load()
|
||||
@ -58,7 +60,7 @@ class AuthStore:
|
||||
|
||||
return self._groups.get(group_id)
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
async def async_get_users(self) -> list[models.User]:
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
@ -66,7 +68,7 @@ class AuthStore:
|
||||
|
||||
return list(self._users.values())
|
||||
|
||||
async def async_get_user(self, user_id: str) -> Optional[models.User]:
|
||||
async def async_get_user(self, user_id: str) -> models.User | None:
|
||||
"""Retrieve a user by id."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
@ -76,12 +78,12 @@ class AuthStore:
|
||||
|
||||
async def async_create_user(
|
||||
self,
|
||||
name: Optional[str],
|
||||
is_owner: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None,
|
||||
group_ids: Optional[List[str]] = None,
|
||||
name: str | None,
|
||||
is_owner: bool | None = None,
|
||||
is_active: bool | None = None,
|
||||
system_generated: bool | None = None,
|
||||
credentials: models.Credentials | None = None,
|
||||
group_ids: list[str] | None = None,
|
||||
) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
@ -97,7 +99,7 @@ class AuthStore:
|
||||
raise ValueError(f"Invalid group specified {group_id}")
|
||||
groups.append(group)
|
||||
|
||||
kwargs: Dict[str, Any] = {
|
||||
kwargs: dict[str, Any] = {
|
||||
"name": name,
|
||||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
@ -146,9 +148,9 @@ class AuthStore:
|
||||
async def async_update_user(
|
||||
self,
|
||||
user: models.User,
|
||||
name: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
group_ids: Optional[List[str]] = None,
|
||||
name: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
group_ids: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Update a user."""
|
||||
assert self._groups is not None
|
||||
@ -203,15 +205,15 @@ class AuthStore:
|
||||
async def async_create_refresh_token(
|
||||
self,
|
||||
user: models.User,
|
||||
client_id: Optional[str] = None,
|
||||
client_name: Optional[str] = None,
|
||||
client_icon: Optional[str] = None,
|
||||
client_id: str | None = None,
|
||||
client_name: str | None = None,
|
||||
client_icon: str | None = None,
|
||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
credential: Optional[models.Credentials] = None,
|
||||
credential: models.Credentials | None = None,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new token for a user."""
|
||||
kwargs: Dict[str, Any] = {
|
||||
kwargs: dict[str, Any] = {
|
||||
"user": user,
|
||||
"client_id": client_id,
|
||||
"token_type": token_type,
|
||||
@ -244,7 +246,7 @@ class AuthStore:
|
||||
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Get refresh token by id."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
@ -259,7 +261,7 @@ class AuthStore:
|
||||
|
||||
async def async_get_refresh_token_by_token(
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
) -> models.RefreshToken | None:
|
||||
"""Get refresh token by token."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
@ -276,7 +278,7 @@ class AuthStore:
|
||||
|
||||
@callback
|
||||
def async_log_refresh_token_usage(
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
self, refresh_token: models.RefreshToken, remote_ip: str | None = None
|
||||
) -> None:
|
||||
"""Update refresh token last used information."""
|
||||
refresh_token.last_used_at = dt_util.utcnow()
|
||||
@ -309,9 +311,9 @@ class AuthStore:
|
||||
self._set_defaults()
|
||||
return
|
||||
|
||||
users: Dict[str, models.User] = OrderedDict()
|
||||
groups: Dict[str, models.Group] = OrderedDict()
|
||||
credentials: Dict[str, models.Credentials] = OrderedDict()
|
||||
users: dict[str, models.User] = OrderedDict()
|
||||
groups: dict[str, models.Group] = OrderedDict()
|
||||
credentials: dict[str, models.Credentials] = OrderedDict()
|
||||
|
||||
# Soft-migrating data as we load. We are going to make sure we have a
|
||||
# read only group and an admin group. There are two states that we can
|
||||
@ -328,7 +330,7 @@ class AuthStore:
|
||||
# was added.
|
||||
|
||||
for group_dict in data.get("groups", []):
|
||||
policy: Optional[PolicyType] = None
|
||||
policy: PolicyType | None = None
|
||||
|
||||
if group_dict["id"] == GROUP_ID_ADMIN:
|
||||
has_admin_group = True
|
||||
@ -489,7 +491,7 @@ class AuthStore:
|
||||
self._store.async_delay_save(self._data_to_save, 1)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> Dict:
|
||||
def _data_to_save(self) -> dict:
|
||||
"""Return the data to store."""
|
||||
assert self._users is not None
|
||||
assert self._groups is not None
|
||||
@ -508,7 +510,7 @@ class AuthStore:
|
||||
|
||||
groups = []
|
||||
for group in self._groups.values():
|
||||
g_dict: Dict[str, Any] = {
|
||||
g_dict: dict[str, Any] = {
|
||||
"id": group.id,
|
||||
# Name not read for sys groups. Kept here for backwards compat
|
||||
"name": group.name,
|
||||
@ -567,7 +569,7 @@ class AuthStore:
|
||||
"""Set default values for auth store."""
|
||||
self._users = OrderedDict()
|
||||
|
||||
groups: Dict[str, models.Group] = OrderedDict()
|
||||
groups: dict[str, models.Group] = OrderedDict()
|
||||
admin_group = _system_admin_group()
|
||||
groups[admin_group.id] = admin_group
|
||||
user_group = _system_user_group()
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
@ -38,7 +38,7 @@ class MultiFactorAuthModule:
|
||||
DEFAULT_TITLE = "Unnamed auth module"
|
||||
MAX_RETRY_TIME = 3
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize an auth module."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
@ -87,7 +87,7 @@ class MultiFactorAuthModule:
|
||||
"""Return whether user is setup."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -104,14 +104,14 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||
self._user_id = user_id
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
errors: Dict[str, str] = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input:
|
||||
result = await self._auth_module.async_setup_user(self._user_id, user_input)
|
||||
@ -125,7 +125,7 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||
|
||||
|
||||
async def auth_mfa_module_from_config(
|
||||
hass: HomeAssistant, config: Dict[str, Any]
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> MultiFactorAuthModule:
|
||||
"""Initialize an auth module from a config."""
|
||||
module_name = config[CONF_TYPE]
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Example auth module."""
|
||||
from typing import Any, Dict
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -28,7 +30,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
|
||||
DEFAULT_TITLE = "Insecure Personal Identify Number"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._data = config["data"]
|
||||
@ -75,17 +77,11 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
for data in self._data:
|
||||
if data["user_id"] == user_id:
|
||||
return True
|
||||
return False
|
||||
return any(data["user_id"] == user_id for data in self._data)
|
||||
|
||||
async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
for data in self._data:
|
||||
if data["user_id"] == user_id:
|
||||
# user_input has been validate in caller
|
||||
if data["pin"] == user_input["pin"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
return any(
|
||||
data["user_id"] == user_id and data["pin"] == user_input["pin"]
|
||||
for data in self._data
|
||||
)
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
Sending HOTP through notify service
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
@ -79,8 +81,8 @@ class NotifySetting:
|
||||
|
||||
secret: str = attr.ib(factory=_generate_secret) # not persistent
|
||||
counter: int = attr.ib(factory=_generate_random) # not persistent
|
||||
notify_service: Optional[str] = attr.ib(default=None)
|
||||
target: Optional[str] = attr.ib(default=None)
|
||||
notify_service: str | None = attr.ib(default=None)
|
||||
target: str | None = attr.ib(default=None)
|
||||
|
||||
|
||||
_UsersDict = Dict[str, NotifySetting]
|
||||
@ -92,10 +94,10 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
|
||||
DEFAULT_TITLE = "Notify One-Time Password"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._user_settings: Optional[_UsersDict] = None
|
||||
self._user_settings: _UsersDict | None = None
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True
|
||||
)
|
||||
@ -146,7 +148,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
)
|
||||
|
||||
@callback
|
||||
def aync_get_available_notify_services(self) -> List[str]:
|
||||
def aync_get_available_notify_services(self) -> list[str]:
|
||||
"""Return list of notify services."""
|
||||
unordered_services = set()
|
||||
|
||||
@ -198,7 +200,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
|
||||
return user_id in self._user_settings
|
||||
|
||||
async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._user_settings is None:
|
||||
await self._async_load()
|
||||
@ -258,7 +260,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
|
||||
)
|
||||
|
||||
async def async_notify(
|
||||
self, code: str, notify_service: str, target: Optional[str] = None
|
||||
self, code: str, notify_service: str, target: str | None = None
|
||||
) -> None:
|
||||
"""Send code by notify service."""
|
||||
data = {"message": self._message_template.format(code)}
|
||||
@ -276,23 +278,23 @@ class NotifySetupFlow(SetupFlow):
|
||||
auth_module: NotifyAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user_id: str,
|
||||
available_notify_services: List[str],
|
||||
available_notify_services: list[str],
|
||||
) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
super().__init__(auth_module, setup_schema, user_id)
|
||||
# to fix typing complaint
|
||||
self._auth_module: NotifyAuthModule = auth_module
|
||||
self._available_notify_services = available_notify_services
|
||||
self._secret: Optional[str] = None
|
||||
self._count: Optional[int] = None
|
||||
self._notify_service: Optional[str] = None
|
||||
self._target: Optional[str] = None
|
||||
self._secret: str | None = None
|
||||
self._count: int | None = None
|
||||
self._notify_service: str | None = None
|
||||
self._target: str | None = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Let user select available notify services."""
|
||||
errors: Dict[str, str] = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
hass = self._auth_module.hass
|
||||
if user_input:
|
||||
@ -306,7 +308,7 @@ class NotifySetupFlow(SetupFlow):
|
||||
if not self._available_notify_services:
|
||||
return self.async_abort(reason="no_available_service")
|
||||
|
||||
schema: Dict[str, Any] = OrderedDict()
|
||||
schema: dict[str, Any] = OrderedDict()
|
||||
schema["notify_service"] = vol.In(self._available_notify_services)
|
||||
schema["target"] = vol.Optional(str)
|
||||
|
||||
@ -315,10 +317,10 @@ class NotifySetupFlow(SetupFlow):
|
||||
)
|
||||
|
||||
async def async_step_setup(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Verify user can receive one-time password."""
|
||||
errors: Dict[str, str] = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
hass = self._auth_module.hass
|
||||
if user_input:
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""Time-based One Time Password auth module."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -50,7 +52,7 @@ def _generate_qr_code(data: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
|
||||
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
||||
"""Generate a secret, url, and QR code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
|
||||
@ -69,10 +71,10 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
DEFAULT_TITLE = "Time-based One Time Password"
|
||||
MAX_RETRY_TIME = 5
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._users: Optional[Dict[str, str]] = None
|
||||
self._users: dict[str, str] | None = None
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True
|
||||
)
|
||||
@ -100,7 +102,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Save data."""
|
||||
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||
|
||||
def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str:
|
||||
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
|
||||
@ -145,7 +147,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
return user_id in self._users # type: ignore
|
||||
|
||||
async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validate(self, user_id: str, user_input: dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
@ -181,13 +183,13 @@ class TotpSetupFlow(SetupFlow):
|
||||
# to fix typing complaint
|
||||
self._auth_module: TotpAuthModule = auth_module
|
||||
self._user = user
|
||||
self._ota_secret: Optional[str] = None
|
||||
self._ota_secret: str | None = None
|
||||
self._url = None # type Optional[str]
|
||||
self._image = None # type Optional[str]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
@ -195,7 +197,7 @@ class TotpSetupFlow(SetupFlow):
|
||||
"""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
|
||||
errors: Dict[str, str] = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job(
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""Auth models."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
from typing import NamedTuple
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
@ -21,7 +23,7 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
||||
class Group:
|
||||
"""A group."""
|
||||
|
||||
name: Optional[str] = attr.ib()
|
||||
name: str | None = attr.ib()
|
||||
policy: perm_mdl.PolicyType = attr.ib()
|
||||
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||
system_generated: bool = attr.ib(default=False)
|
||||
@ -31,24 +33,24 @@ class Group:
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
name: Optional[str] = attr.ib()
|
||||
name: str | None = attr.ib()
|
||||
perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False)
|
||||
id: str = attr.ib(factory=lambda: uuid.uuid4().hex)
|
||||
is_owner: bool = attr.ib(default=False)
|
||||
is_active: bool = attr.ib(default=False)
|
||||
system_generated: bool = attr.ib(default=False)
|
||||
|
||||
groups: List[Group] = attr.ib(factory=list, eq=False, order=False)
|
||||
groups: list[Group] = attr.ib(factory=list, eq=False, order=False)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials: List["Credentials"] = attr.ib(factory=list, eq=False, order=False)
|
||||
credentials: list[Credentials] = attr.ib(factory=list, eq=False, order=False)
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens: Dict[str, "RefreshToken"] = attr.ib(
|
||||
refresh_tokens: dict[str, RefreshToken] = attr.ib(
|
||||
factory=dict, eq=False, order=False
|
||||
)
|
||||
|
||||
_permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib(
|
||||
_permissions: perm_mdl.PolicyPermissions | None = attr.ib(
|
||||
init=False,
|
||||
eq=False,
|
||||
order=False,
|
||||
@ -89,10 +91,10 @@ class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user: User = attr.ib()
|
||||
client_id: Optional[str] = attr.ib()
|
||||
client_id: str | None = attr.ib()
|
||||
access_token_expiration: timedelta = attr.ib()
|
||||
client_name: Optional[str] = attr.ib(default=None)
|
||||
client_icon: Optional[str] = attr.ib(default=None)
|
||||
client_name: str | None = attr.ib(default=None)
|
||||
client_icon: str | None = attr.ib(default=None)
|
||||
token_type: str = attr.ib(
|
||||
default=TOKEN_TYPE_NORMAL,
|
||||
validator=attr.validators.in_(
|
||||
@ -104,12 +106,12 @@ class RefreshToken:
|
||||
token: str = attr.ib(factory=lambda: secrets.token_hex(64))
|
||||
jwt_key: str = attr.ib(factory=lambda: secrets.token_hex(64))
|
||||
|
||||
last_used_at: Optional[datetime] = attr.ib(default=None)
|
||||
last_used_ip: Optional[str] = attr.ib(default=None)
|
||||
last_used_at: datetime | None = attr.ib(default=None)
|
||||
last_used_ip: str | None = attr.ib(default=None)
|
||||
|
||||
credential: Optional["Credentials"] = attr.ib(default=None)
|
||||
credential: Credentials | None = attr.ib(default=None)
|
||||
|
||||
version: Optional[str] = attr.ib(default=__version__)
|
||||
version: str | None = attr.ib(default=__version__)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@ -117,7 +119,7 @@ class Credentials:
|
||||
"""Credentials for a user on an auth provider."""
|
||||
|
||||
auth_provider_type: str = attr.ib()
|
||||
auth_provider_id: Optional[str] = attr.ib()
|
||||
auth_provider_id: str | None = attr.ib()
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data: dict = attr.ib()
|
||||
@ -129,5 +131,5 @@ class Credentials:
|
||||
class UserMeta(NamedTuple):
|
||||
"""User metadata."""
|
||||
|
||||
name: Optional[str]
|
||||
name: str | None
|
||||
is_active: bool
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Permissions for Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Callable, Optional
|
||||
from typing import Any, Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -19,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
_cached_entity_func: Optional[Callable[[str, str], bool]] = None
|
||||
_cached_entity_func: Callable[[str, str], bool] | None = None
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Entity permissions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -43,14 +45,14 @@ ENTITY_POLICY_SCHEMA = vol.Any(
|
||||
|
||||
def _lookup_domain(
|
||||
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
|
||||
) -> Optional[ValueType]:
|
||||
) -> ValueType | None:
|
||||
"""Look up entity permissions by domain."""
|
||||
return domains_dict.get(entity_id.split(".", 1)[0])
|
||||
|
||||
|
||||
def _lookup_area(
|
||||
perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str
|
||||
) -> Optional[ValueType]:
|
||||
) -> ValueType | None:
|
||||
"""Look up entity permissions by area."""
|
||||
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
|
||||
|
||||
@ -67,7 +69,7 @@ def _lookup_area(
|
||||
|
||||
def _lookup_device(
|
||||
perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str
|
||||
) -> Optional[ValueType]:
|
||||
) -> ValueType | None:
|
||||
"""Look up entity permissions by device."""
|
||||
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
|
||||
|
||||
@ -79,7 +81,7 @@ def _lookup_device(
|
||||
|
||||
def _lookup_entity_id(
|
||||
perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str
|
||||
) -> Optional[ValueType]:
|
||||
) -> ValueType | None:
|
||||
"""Look up entity permission by entity id."""
|
||||
return entities_dict.get(entity_id)
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
"""Merging of policies."""
|
||||
from typing import Dict, List, Set, cast
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from .types import CategoryType, PolicyType
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
def merge_policies(policies: list[PolicyType]) -> PolicyType:
|
||||
"""Merge policies."""
|
||||
new_policy: Dict[str, CategoryType] = {}
|
||||
seen: Set[str] = set()
|
||||
new_policy: dict[str, CategoryType] = {}
|
||||
seen: set[str] = set()
|
||||
for policy in policies:
|
||||
for category in policy:
|
||||
if category in seen:
|
||||
@ -20,7 +22,7 @@ def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
return new_policy
|
||||
|
||||
|
||||
def _merge_policies(sources: List[CategoryType]) -> CategoryType:
|
||||
def _merge_policies(sources: list[CategoryType]) -> CategoryType:
|
||||
"""Merge a policy."""
|
||||
# When merging policies, the most permissive wins.
|
||||
# This means we order it like this:
|
||||
@ -34,7 +36,7 @@ def _merge_policies(sources: List[CategoryType]) -> CategoryType:
|
||||
# merge each key in the source.
|
||||
|
||||
policy: CategoryType = None
|
||||
seen: Set[str] = set()
|
||||
seen: set[str] = set()
|
||||
for source in sources:
|
||||
if source is None:
|
||||
continue
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""Models for permissions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.helpers import ( # noqa: F401
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dev_reg,
|
||||
entity_registry as ent_reg,
|
||||
)
|
||||
@ -15,5 +16,5 @@ if TYPE_CHECKING:
|
||||
class PermissionLookup:
|
||||
"""Class to hold data for permission lookups."""
|
||||
|
||||
entity_registry: "ent_reg.EntityRegistry" = attr.ib()
|
||||
device_registry: "dev_reg.DeviceRegistry" = attr.ib()
|
||||
entity_registry: ent_reg.EntityRegistry = attr.ib()
|
||||
device_registry: dev_reg.DeviceRegistry = attr.ib()
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Helpers to deal with permissions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
from typing import Callable, Dict, List, Optional, cast
|
||||
from typing import Callable, Dict, Optional, cast
|
||||
|
||||
from .const import SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
@ -45,7 +47,7 @@ def compile_policy(
|
||||
|
||||
assert isinstance(policy, dict)
|
||||
|
||||
funcs: List[Callable[[str, str], Optional[bool]]] = []
|
||||
funcs: list[Callable[[str, str], bool | None]] = []
|
||||
|
||||
for key, lookup_func in subcategories.items():
|
||||
lookup_value = policy.get(key)
|
||||
@ -80,10 +82,10 @@ def compile_policy(
|
||||
|
||||
def _gen_dict_test_func(
|
||||
perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict
|
||||
) -> Callable[[str, str], Optional[bool]]:
|
||||
) -> Callable[[str, str], bool | None]:
|
||||
"""Generate a lookup function."""
|
||||
|
||||
def test_value(object_id: str, key: str) -> Optional[bool]:
|
||||
def test_value(object_id: str, key: str) -> bool | None:
|
||||
"""Test if permission is allowed based on the keys."""
|
||||
schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id)
|
||||
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
@ -42,7 +42,7 @@ class AuthProvider:
|
||||
DEFAULT_TITLE = "Unnamed auth provider"
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||
self, hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
@ -50,7 +50,7 @@ class AuthProvider:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[str]:
|
||||
def id(self) -> str | None:
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
@ -72,7 +72,7 @@ class AuthProvider:
|
||||
"""Return whether multi-factor auth supported by the auth provider."""
|
||||
return True
|
||||
|
||||
async def async_credentials(self) -> List[Credentials]:
|
||||
async def async_credentials(self) -> list[Credentials]:
|
||||
"""Return all credentials of this provider."""
|
||||
users = await self.store.async_get_users()
|
||||
return [
|
||||
@ -86,7 +86,7 @@ class AuthProvider:
|
||||
]
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
|
||||
def async_create_credentials(self, data: dict[str, str]) -> Credentials:
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type, auth_provider_id=self.id, data=data
|
||||
@ -94,7 +94,7 @@ class AuthProvider:
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: dict | None) -> LoginFlow:
|
||||
"""Return the data flow for logging in with auth provider.
|
||||
|
||||
Auth provider should extend LoginFlow and return an instance.
|
||||
@ -102,7 +102,7 @@ class AuthProvider:
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]
|
||||
self, flow_result: dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
@ -121,7 +121,7 @@ class AuthProvider:
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
|
||||
self, refresh_token: RefreshToken, remote_ip: str | None = None
|
||||
) -> None:
|
||||
"""Verify a refresh token is still valid.
|
||||
|
||||
@ -131,7 +131,7 @@ class AuthProvider:
|
||||
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||
hass: HomeAssistant, store: AuthStore, config: dict[str, Any]
|
||||
) -> AuthProvider:
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
@ -188,17 +188,17 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
def __init__(self, auth_provider: AuthProvider) -> None:
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
self._auth_module_id: Optional[str] = None
|
||||
self._auth_module_id: str | None = None
|
||||
self._auth_manager = auth_provider.hass.auth
|
||||
self.available_mfa_modules: Dict[str, str] = {}
|
||||
self.available_mfa_modules: dict[str, str] = {}
|
||||
self.created_at = dt_util.utcnow()
|
||||
self.invalid_mfa_times = 0
|
||||
self.user: Optional[User] = None
|
||||
self.credential: Optional[Credentials] = None
|
||||
self.user: User | None = None
|
||||
self.credential: Credentials | None = None
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the first step of login flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
@ -207,8 +207,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_step_select_mfa_module(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of select mfa module."""
|
||||
errors = {}
|
||||
|
||||
@ -232,8 +232,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
)
|
||||
|
||||
async def async_step_mfa(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of mfa validation."""
|
||||
assert self.credential
|
||||
assert self.user
|
||||
@ -273,7 +273,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
if not errors:
|
||||
return await self.async_finish(self.credential)
|
||||
|
||||
description_placeholders: Dict[str, Optional[str]] = {
|
||||
description_placeholders: dict[str, str | None] = {
|
||||
"mfa_module_name": auth_module.name,
|
||||
"mfa_module_id": auth_module.id,
|
||||
}
|
||||
@ -285,6 +285,6 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_finish(self, flow_result: Any) -> Dict:
|
||||
async def async_finish(self, flow_result: Any) -> dict:
|
||||
"""Handle the pass of login flow."""
|
||||
return self.async_create_entry(title=self._auth_provider.name, data=flow_result)
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""Auth provider that validates credentials via an external command."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio.subprocess
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -51,9 +52,9 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
attributes provided by external programs.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._user_meta: Dict[str, Dict[str, Any]] = {}
|
||||
self._user_meta: dict[str, dict[str, Any]] = {}
|
||||
|
||||
async def async_login_flow(self, context: Optional[dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: dict | None) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return CommandLineLoginFlow(self)
|
||||
|
||||
@ -82,7 +83,7 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
raise InvalidAuthError
|
||||
|
||||
if self.config[CONF_META]:
|
||||
meta: Dict[str, str] = {}
|
||||
meta: dict[str, str] = {}
|
||||
for _line in stdout.splitlines():
|
||||
try:
|
||||
line = _line.decode().lstrip()
|
||||
@ -99,7 +100,7 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
self._user_meta[username] = meta
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]
|
||||
self, flow_result: dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result["username"]
|
||||
@ -125,8 +126,8 @@ class CommandLineLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
@ -143,7 +144,7 @@ class CommandLineLoginFlow(LoginFlow):
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema: Dict[str, type] = collections.OrderedDict()
|
||||
schema: dict[str, type] = collections.OrderedDict()
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
|
@ -5,7 +5,7 @@ import asyncio
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Set, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
@ -21,7 +21,7 @@ STORAGE_VERSION = 1
|
||||
STORAGE_KEY = "auth_provider.homeassistant"
|
||||
|
||||
|
||||
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def _disallow_id(conf: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Disallow ID in config."""
|
||||
if CONF_ID in conf:
|
||||
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
|
||||
@ -62,7 +62,7 @@ class Data:
|
||||
self._store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY, private=True
|
||||
)
|
||||
self._data: Optional[Dict[str, Any]] = None
|
||||
self._data: dict[str, Any] | None = None
|
||||
# Legacy mode will allow usernames to start/end with whitespace
|
||||
# and will compare usernames case-insensitive.
|
||||
# Remove in 2020 or when we launch 1.0.
|
||||
@ -83,7 +83,7 @@ class Data:
|
||||
if data is None:
|
||||
data = {"users": []}
|
||||
|
||||
seen: Set[str] = set()
|
||||
seen: set[str] = set()
|
||||
|
||||
for user in data["users"]:
|
||||
username = user["username"]
|
||||
@ -121,7 +121,7 @@ class Data:
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def users(self) -> List[Dict[str, str]]:
|
||||
def users(self) -> list[dict[str, str]]:
|
||||
"""Return users."""
|
||||
return self._data["users"] # type: ignore
|
||||
|
||||
@ -220,7 +220,7 @@ class HassAuthProvider(AuthProvider):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize an Home Assistant auth provider."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.data: Optional[Data] = None
|
||||
self.data: Data | None = None
|
||||
self._init_lock = asyncio.Lock()
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
@ -233,7 +233,7 @@ class HassAuthProvider(AuthProvider):
|
||||
await data.async_load()
|
||||
self.data = data
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: dict | None) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return HassLoginFlow(self)
|
||||
|
||||
@ -277,7 +277,7 @@ class HassAuthProvider(AuthProvider):
|
||||
await self.data.async_save()
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]
|
||||
self, flow_result: dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
if self.data is None:
|
||||
@ -318,8 +318,8 @@ class HassLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
@ -335,7 +335,7 @@ class HassLoginFlow(LoginFlow):
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema: Dict[str, type] = OrderedDict()
|
||||
schema: dict[str, type] = OrderedDict()
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""Example auth provider."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
import hmac
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -33,7 +35,7 @@ class InvalidAuthError(HomeAssistantError):
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: dict | None) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return ExampleLoginFlow(self)
|
||||
|
||||
@ -60,7 +62,7 @@ class ExampleAuthProvider(AuthProvider):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]
|
||||
self, flow_result: dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result["username"]
|
||||
@ -94,8 +96,8 @@ class ExampleLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
@ -111,7 +113,7 @@ class ExampleLoginFlow(LoginFlow):
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema: Dict[str, type] = OrderedDict()
|
||||
schema: dict[str, type] = OrderedDict()
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
|
@ -3,8 +3,10 @@ Support Legacy API password auth provider.
|
||||
|
||||
It will be removed when auth system production ready
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -40,7 +42,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""Return api_password."""
|
||||
return str(self.config[CONF_API_PASSWORD])
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: dict | None) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return LegacyLoginFlow(self)
|
||||
|
||||
@ -55,7 +57,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]
|
||||
self, flow_result: dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Return credentials for this login."""
|
||||
credentials = await self.async_credentials()
|
||||
@ -79,8 +81,8 @@ class LegacyLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
It shows list of users if access from trusted network.
|
||||
Abort login flow if not access from trusted network.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Network,
|
||||
@ -11,7 +13,7 @@ from ipaddress import (
|
||||
ip_address,
|
||||
ip_network,
|
||||
)
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
from typing import Any, Dict, List, Union, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -68,12 +70,12 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
DEFAULT_TITLE = "Trusted Networks"
|
||||
|
||||
@property
|
||||
def trusted_networks(self) -> List[IPNetwork]:
|
||||
def trusted_networks(self) -> list[IPNetwork]:
|
||||
"""Return trusted networks."""
|
||||
return cast(List[IPNetwork], self.config[CONF_TRUSTED_NETWORKS])
|
||||
|
||||
@property
|
||||
def trusted_users(self) -> Dict[IPNetwork, Any]:
|
||||
def trusted_users(self) -> dict[IPNetwork, Any]:
|
||||
"""Return trusted users per network."""
|
||||
return cast(Dict[IPNetwork, Any], self.config[CONF_TRUSTED_USERS])
|
||||
|
||||
@ -82,7 +84,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
"""Trusted Networks auth provider does not support MFA."""
|
||||
return False
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: dict | None) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
assert context is not None
|
||||
ip_addr = cast(IPAddress, context.get("ip_address"))
|
||||
@ -111,7 +113,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
if (
|
||||
user.id in user_list
|
||||
or any(
|
||||
[group.id in flattened_group_list for group in user.groups]
|
||||
group.id in flattened_group_list for group in user.groups
|
||||
)
|
||||
)
|
||||
]
|
||||
@ -125,7 +127,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
)
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]
|
||||
self, flow_result: dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
user_id = flow_result["user"]
|
||||
@ -169,7 +171,7 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||
|
||||
@callback
|
||||
def async_validate_refresh_token(
|
||||
self, refresh_token: RefreshToken, remote_ip: Optional[str] = None
|
||||
self, refresh_token: RefreshToken, remote_ip: str | None = None
|
||||
) -> None:
|
||||
"""Verify a refresh token is still valid."""
|
||||
if remote_ip is None:
|
||||
@ -186,7 +188,7 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
self,
|
||||
auth_provider: TrustedNetworksAuthProvider,
|
||||
ip_addr: IPAddress,
|
||||
available_users: Dict[str, Optional[str]],
|
||||
available_users: dict[str, str | None],
|
||||
allow_bypass_login: bool,
|
||||
) -> None:
|
||||
"""Initialize the login flow."""
|
||||
@ -196,8 +198,8 @@ class TrustedNetworksLoginFlow(LoginFlow):
|
||||
self._allow_bypass_login = allow_bypass_login
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
try:
|
||||
cast(
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
@ -8,7 +10,7 @@ import os
|
||||
import sys
|
||||
import threading
|
||||
from time import monotonic
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
@ -28,7 +30,6 @@ from homeassistant.setup import (
|
||||
from homeassistant.util.async_ import gather_with_concurrency
|
||||
from homeassistant.util.logging import async_activate_log_queue_handler
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
@ -75,8 +76,8 @@ STAGE_1_INTEGRATIONS = {
|
||||
|
||||
|
||||
async def async_setup_hass(
|
||||
runtime_config: "RuntimeConfig",
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
runtime_config: RuntimeConfig,
|
||||
) -> core.HomeAssistant | None:
|
||||
"""Set up Home Assistant."""
|
||||
hass = core.HomeAssistant()
|
||||
hass.config.config_dir = runtime_config.config_dir
|
||||
@ -122,8 +123,6 @@ async def async_setup_hass(
|
||||
basic_setup_success = (
|
||||
await async_from_config_dict(config_dict, hass) is not None
|
||||
)
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
if config_dict is None:
|
||||
safe_mode = True
|
||||
@ -191,7 +190,7 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||
|
||||
async def async_from_config_dict(
|
||||
config: ConfigType, hass: core.HomeAssistant
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
) -> core.HomeAssistant | None:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
@ -258,8 +257,8 @@ async def async_from_config_dict(
|
||||
def async_enable_logging(
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days: Optional[int] = None,
|
||||
log_file: Optional[str] = None,
|
||||
log_rotate_days: int | None = None,
|
||||
log_file: str | None = None,
|
||||
log_no_color: bool = False,
|
||||
) -> None:
|
||||
"""Set up the logging.
|
||||
@ -365,7 +364,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
|
||||
|
||||
@core.callback
|
||||
def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
|
||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
domains = {key.split(" ")[0] for key in config if key != core.DOMAIN}
|
||||
@ -382,7 +381,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
|
||||
|
||||
|
||||
async def _async_log_pending_setups(
|
||||
hass: core.HomeAssistant, domains: Set[str], setup_started: Dict[str, datetime]
|
||||
hass: core.HomeAssistant, domains: set[str], setup_started: dict[str, datetime]
|
||||
) -> None:
|
||||
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
|
||||
while True:
|
||||
@ -399,9 +398,9 @@ async def _async_log_pending_setups(
|
||||
|
||||
async def async_setup_multi_components(
|
||||
hass: core.HomeAssistant,
|
||||
domains: Set[str],
|
||||
config: Dict[str, Any],
|
||||
setup_started: Dict[str, datetime],
|
||||
domains: set[str],
|
||||
config: dict[str, Any],
|
||||
setup_started: dict[str, datetime],
|
||||
) -> None:
|
||||
"""Set up multiple domains. Log on failure."""
|
||||
futures = {
|
||||
@ -425,7 +424,7 @@ async def async_setup_multi_components(
|
||||
|
||||
|
||||
async def _async_set_up_integrations(
|
||||
hass: core.HomeAssistant, config: Dict[str, Any]
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Set up all the integrations."""
|
||||
setup_started = hass.data[DATA_SETUP_STARTED] = {}
|
||||
@ -433,7 +432,7 @@ async def _async_set_up_integrations(
|
||||
|
||||
# Resolve all dependencies so we know all integrations
|
||||
# that will have to be loaded and start rightaway
|
||||
integration_cache: Dict[str, loader.Integration] = {}
|
||||
integration_cache: dict[str, loader.Integration] = {}
|
||||
to_resolve = domains_to_setup
|
||||
while to_resolve:
|
||||
old_to_resolve = to_resolve
|
||||
|
@ -66,7 +66,7 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
ABODE_PLATFORMS = [
|
||||
PLATFORMS = [
|
||||
"alarm_control_panel",
|
||||
"binary_sensor",
|
||||
"lock",
|
||||
@ -138,7 +138,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
||||
|
||||
for platform in ABODE_PLATFORMS:
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
||||
)
|
||||
@ -158,7 +158,7 @@ async def async_unload_entry(hass, config_entry):
|
||||
|
||||
tasks = []
|
||||
|
||||
for platform in ABODE_PLATFORMS:
|
||||
for platform in PLATFORMS:
|
||||
tasks.append(
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
)
|
||||
@ -363,7 +363,7 @@ class AbodeDevice(AbodeEntity):
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
@ -411,7 +411,7 @@ class AbodeAutomation(AbodeEntity):
|
||||
return self._automation.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"}
|
||||
|
||||
|
@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
|
||||
self._device.set_away()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
|
@ -8,7 +8,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST
|
||||
|
||||
from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import
|
||||
from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER
|
||||
|
||||
CONF_MFA = "mfa_code"
|
||||
CONF_POLLING = "polling"
|
||||
@ -163,7 +163,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
if self._async_current_entries():
|
||||
LOGGER.warning("Already configured. Only a single configuration possible.")
|
||||
LOGGER.warning("Already configured; Only a single configuration possible")
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
self._polling = import_config.get(CONF_POLLING, False)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Support for Abode Security System sensors."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbodeSensor(AbodeDevice):
|
||||
class AbodeSensor(AbodeDevice, SensorEntity):
|
||||
"""A sensor implementation for Abode devices."""
|
||||
|
||||
def __init__(self, data, device, sensor_type):
|
||||
|
12
homeassistant/components/abode/translations/fa.json
Normal file
12
homeassistant/components/abode/translations/fa.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\u06a9\u0644\u0645\u0647 \u0639\u0628\u0648\u0631",
|
||||
"username": "\u0627\u06cc\u0645\u06cc\u0644"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
homeassistant/components/abode/translations/he.json
Normal file
11
homeassistant/components/abode/translations/he.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
|
||||
"reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt",
|
||||
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
|
||||
"invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s",
|
||||
"invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA k\u00f3d (6 jegy\u0171)"
|
||||
},
|
||||
"title": "Add meg az Abode MFA k\u00f3dj\u00e1t"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Jelsz\u00f3",
|
||||
"username": "E-mail"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
35
homeassistant/components/abode/translations/id.json
Normal file
35
homeassistant/components/abode/translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,18 +2,26 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
"invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_mfa_code": "MFA \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA \ucf54\ub4dc (6\uc790\ub9ac)"
|
||||
},
|
||||
"title": "Abode\uc5d0 \ub300\ud55c MFA \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "\ube44\ubc00\ubc88\ud638",
|
||||
"username": "\uc774\uba54\uc77c"
|
||||
}
|
||||
},
|
||||
"title": "Abode \ub85c\uadf8\uc778 \uc815\ubcf4 \uc785\ub825\ud558\uae30"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -8,8 +8,6 @@ from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@ -26,12 +24,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = ["sensor", "weather"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||
"""Set up configured AccuWeather."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry) -> bool:
|
||||
"""Set up AccuWeather as config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
@ -45,21 +37,18 @@ async def async_setup_entry(hass, config_entry) -> bool:
|
||||
coordinator = AccuWeatherDataUpdateCoordinator(
|
||||
hass, websession, api_key, location_key, forecast
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
undo_listener = config_entry.add_update_listener(update_listener)
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = {
|
||||
COORDINATOR: coordinator,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
||||
)
|
||||
|
||||
return True
|
||||
@ -70,8 +59,8 @@ async def async_unload_entry(hass, config_entry):
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in PLATFORMS
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import
|
||||
from .const import CONF_FORECAST, DOMAIN
|
||||
|
||||
|
||||
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "accuweather",
|
||||
"name": "AccuWeather",
|
||||
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
|
||||
"requirements": ["accuweather==0.1.0"],
|
||||
"requirements": ["accuweather==0.1.1"],
|
||||
"codeowners": ["@bieniu"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum"
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Support for the AccuWeather service."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
@ -48,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async_add_entities(sensors, False)
|
||||
|
||||
|
||||
class AccuWeatherSensor(CoordinatorEntity):
|
||||
class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
def __init__(self, name, kind, coordinator, forecast_day=None):
|
||||
@ -141,7 +142,7 @@ class AccuWeatherSensor(CoordinatorEntity):
|
||||
return SENSOR_TYPES[self.kind][self._unit_system]
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self.forecast_day is not None:
|
||||
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]:
|
||||
|
@ -5,7 +5,8 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel"
|
||||
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
|
||||
"requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Sie m\u00fcssen warten oder den API-Schl\u00fcssel \u00e4ndern."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
11
homeassistant/components/accuweather/translations/he.json
Normal file
11
homeassistant/components/accuweather/translations/he.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
homeassistant/components/accuweather/translations/hu.json
Normal file
29
homeassistant/components/accuweather/translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
homeassistant/components/accuweather/translations/id.json
Normal file
41
homeassistant/components/accuweather/translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
|
||||
"invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"requests_exceeded": "Accuweather API\uc5d0 \ud5c8\uc6a9\ub41c \uc694\uccad \uc218\uac00 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uae30\ub2e4\ub9ac\uac70\ub098 API \ud0a4\ub97c \ubcc0\uacbd\ud574\uc57c \ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
@ -15,15 +16,26 @@
|
||||
"longitude": "\uacbd\ub3c4",
|
||||
"name": "\uc774\ub984"
|
||||
},
|
||||
"description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
"description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
|
||||
"title": "AccuWeather"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4."
|
||||
"data": {
|
||||
"forecast": "\ub0a0\uc528 \uc608\ubcf4"
|
||||
},
|
||||
"description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4.",
|
||||
"title": "AccuWeather \uc635\uc158"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "AccuWeather \uc11c\ubc84 \uc5f0\uacb0",
|
||||
"remaining_requests": "\ub0a8\uc740 \ud5c8\uc6a9 \uc694\uccad \ud69f\uc218"
|
||||
}
|
||||
}
|
||||
}
|
@ -31,5 +31,11 @@
|
||||
"title": "AccuWeather-opties"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Kan AccuWeather server bereiken",
|
||||
"remaining_requests": "Resterende toegestane verzoeken"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Cs\u00f6kken\u0151",
|
||||
"rising": "Emelked\u0151",
|
||||
"steady": "\u00c1lland\u00f3"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Turun",
|
||||
"rising": "Naik",
|
||||
"steady": "Tetap"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "\ud558\uac15",
|
||||
"rising": "\uc0c1\uc2b9",
|
||||
"steady": "\uc548\uc815"
|
||||
}
|
||||
}
|
||||
}
|
@ -132,7 +132,7 @@ class AcerSwitch(SwitchEntity):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return state attributes."""
|
||||
return self._attributes
|
||||
|
||||
|
@ -11,11 +11,6 @@ CONF_HUBS = "hubs"
|
||||
PLATFORMS = ["cover", "sensor"]
|
||||
|
||||
|
||||
async def async_setup(hass: core.HomeAssistant, config: dict):
|
||||
"""Set up the Rollease Acmeda Automate component."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||
):
|
||||
@ -28,9 +23,9 @@ async def async_setup_entry(
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = hub
|
||||
|
||||
for component in PLATFORMS:
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
||||
)
|
||||
|
||||
return True
|
||||
@ -45,8 +40,8 @@ async def async_unload_entry(
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in PLATFORMS
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Config flow for Rollease Acmeda Automate Pulse Hub."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Optional
|
||||
from contextlib import suppress
|
||||
|
||||
import aiopulse
|
||||
import async_timeout
|
||||
@ -8,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@ -19,7 +21,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self.discovered_hubs: Optional[Dict[str, aiopulse.Hub]] = None
|
||||
self.discovered_hubs: dict[str, aiopulse.Hub] | None = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
@ -36,15 +38,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
|
||||
hubs = []
|
||||
try:
|
||||
with async_timeout.timeout(5):
|
||||
with suppress(asyncio.TimeoutError):
|
||||
async with async_timeout.timeout(5):
|
||||
async for hub in aiopulse.Hub.discover():
|
||||
if hub.id not in already_configured:
|
||||
hubs.append(hub)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
if len(hubs) == 0:
|
||||
if not hubs:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
if len(hubs) == 1:
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Code to handle a Pulse Hub."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import aiopulse
|
||||
|
||||
@ -17,7 +18,7 @@ class PulseHub:
|
||||
"""Initialize the system."""
|
||||
self.config_entry = config_entry
|
||||
self.hass = hass
|
||||
self.api: Optional[aiopulse.Hub] = None
|
||||
self.api: aiopulse.Hub | None = None
|
||||
self.tasks = []
|
||||
self.current_rollers = {}
|
||||
self.cleanup_callbacks = []
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Support for Acmeda Roller Blind Batteries."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@ -29,7 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
)
|
||||
|
||||
|
||||
class AcmedaBattery(AcmedaBase):
|
||||
class AcmedaBattery(AcmedaBase, SensorEntity):
|
||||
"""Representation of a Acmeda cover device."""
|
||||
|
||||
device_class = DEVICE_CLASS_BATTERY
|
||||
|
7
homeassistant/components/acmeda/translations/hu.json
Normal file
7
homeassistant/components/acmeda/translations/hu.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton"
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/acmeda/translations/id.json
Normal file
15
homeassistant/components/acmeda/translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ class ActiontecDeviceScanner(DeviceScanner):
|
||||
self.last_results = []
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
_LOGGER.info("canner initialized")
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Support for AdGuard Home."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
|
||||
import voluptuous as vol
|
||||
@ -27,11 +29,11 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -43,13 +45,10 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
"""Set up the AdGuard Home components."""
|
||||
return True
|
||||
PLATFORMS = ["sensor", "switch"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AdGuard Home from a config entry."""
|
||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||
adguard = AdGuardHome(
|
||||
@ -69,32 +68,36 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
except AdGuardHomeConnectionError as exception:
|
||||
raise ConfigEntryNotReady from exception
|
||||
|
||||
for component in "sensor", "switch":
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||
)
|
||||
|
||||
async def add_url(call) -> None:
|
||||
"""Service call to add a new filter subscription to AdGuard Home."""
|
||||
await adguard.filtering.add_url(
|
||||
call.data.get(CONF_NAME), call.data.get(CONF_URL)
|
||||
allowlist=False, name=call.data.get(CONF_NAME), url=call.data.get(CONF_URL)
|
||||
)
|
||||
|
||||
async def remove_url(call) -> None:
|
||||
"""Service call to remove a filter subscription from AdGuard Home."""
|
||||
await adguard.filtering.remove_url(call.data.get(CONF_URL))
|
||||
await adguard.filtering.remove_url(allowlist=False, url=call.data.get(CONF_URL))
|
||||
|
||||
async def enable_url(call) -> None:
|
||||
"""Service call to enable a filter subscription in AdGuard Home."""
|
||||
await adguard.filtering.enable_url(call.data.get(CONF_URL))
|
||||
await adguard.filtering.enable_url(allowlist=False, url=call.data.get(CONF_URL))
|
||||
|
||||
async def disable_url(call) -> None:
|
||||
"""Service call to disable a filter subscription in AdGuard Home."""
|
||||
await adguard.filtering.disable_url(call.data.get(CONF_URL))
|
||||
await adguard.filtering.disable_url(
|
||||
allowlist=False, url=call.data.get(CONF_URL)
|
||||
)
|
||||
|
||||
async def refresh(call) -> None:
|
||||
"""Service call to refresh the filter subscriptions in AdGuard Home."""
|
||||
await adguard.filtering.refresh(call.data.get(CONF_FORCE))
|
||||
await adguard.filtering.refresh(
|
||||
allowlist=False, force=call.data.get(CONF_FORCE)
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
|
||||
@ -115,7 +118,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload AdGuard Home config entry."""
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
@ -123,8 +126,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
||||
|
||||
for component in "sensor", "switch":
|
||||
await hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for platform in PLATFORMS:
|
||||
await hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||
|
||||
del hass.data[DOMAIN]
|
||||
|
||||
@ -189,7 +192,7 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
|
||||
"""Defines a AdGuard Home device entity."""
|
||||
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
def device_info(self) -> dict[str, Any]:
|
||||
"""Return device information about this AdGuard Home instance."""
|
||||
return {
|
||||
"identifiers": {
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Config flow to configure the AdGuard Home integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.adguard.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@ -15,9 +18,10 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
|
||||
class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a AdGuard Home config flow."""
|
||||
|
||||
VERSION = 1
|
||||
@ -25,7 +29,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
|
||||
_hassio_discovery = None
|
||||
|
||||
async def _show_setup_form(self, errors=None):
|
||||
async def _show_setup_form(
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@ -42,7 +48,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def _show_hassio_form(self, errors=None):
|
||||
async def _show_hassio_form(
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Show the Hass.io confirmation form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="hassio_confirm",
|
||||
@ -51,7 +59,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Handle a flow initiated by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
@ -91,7 +101,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_hassio(self, discovery_info):
|
||||
async def async_step_hassio(self, discovery_info: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Prepare configuration for a Hass.io AdGuard Home add-on.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
@ -100,6 +110,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
|
||||
if not entries:
|
||||
self._hassio_discovery = discovery_info
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
cur_entry = entries[0]
|
||||
@ -129,7 +140,9 @@ class AdGuardHomeFlowHandler(ConfigFlow):
|
||||
|
||||
return self.async_abort(reason="existing_instance_updated")
|
||||
|
||||
async def async_step_hassio_confirm(self, user_input=None):
|
||||
async def async_step_hassio_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Confirm Hass.io discovery."""
|
||||
if user_input is None:
|
||||
return await self._show_hassio_form()
|
||||
|
@ -3,6 +3,6 @@
|
||||
"name": "AdGuard Home",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adguard",
|
||||
"requirements": ["adguardhome==0.4.2"],
|
||||
"requirements": ["adguardhome==0.5.0"],
|
||||
"codeowners": ["@frenck"]
|
||||
}
|
||||
|
@ -1,25 +1,29 @@
|
||||
"""Support for AdGuard Home sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Callable
|
||||
|
||||
from adguardhome import AdGuardHomeConnectionError
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
|
||||
from homeassistant.components.adguard import AdGuardHomeDeviceEntity
|
||||
from homeassistant.components.adguard.const import (
|
||||
DATA_ADGUARD_CLIENT,
|
||||
DATA_ADGUARD_VERION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import AdGuardHomeDeviceEntity
|
||||
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=300)
|
||||
PARALLEL_UPDATES = 4
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[list[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up AdGuard Home sensor based on a config entry."""
|
||||
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
|
||||
@ -45,12 +49,12 @@ async def async_setup_entry(
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
|
||||
class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity):
|
||||
"""Defines a AdGuard Home sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
adguard,
|
||||
adguard: AdGuardHome,
|
||||
name: str,
|
||||
icon: str,
|
||||
measurement: str,
|
||||
@ -78,12 +82,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@ -91,7 +95,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
|
||||
class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home DNS Queries sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries"
|
||||
@ -105,7 +109,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home blocked by filtering sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -124,7 +128,7 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home blocked percentage sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -143,7 +147,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home replaced by parental control sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -161,7 +165,7 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home replaced by safe browsing sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -179,7 +183,7 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home replaced by safe search sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -197,7 +201,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home average processing time sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -216,7 +220,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
|
||||
class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
|
||||
"""Defines a AdGuard Home rules count sensor."""
|
||||
|
||||
def __init__(self, adguard):
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
@ -229,4 +233,4 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
self._state = await self.adguard.filtering.rules_count()
|
||||
self._state = await self.adguard.filtering.rules_count(allowlist=False)
|
||||
|
@ -13,8 +13,8 @@
|
||||
}
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "AdGuard Home via Hass.io add-on",
|
||||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?"
|
||||
"title": "AdGuard Home via Home Assistant add-on",
|
||||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
@ -1,19 +1,20 @@
|
||||
"""Support for AdGuard Home switches."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
|
||||
|
||||
from homeassistant.components.adguard import AdGuardHomeDeviceEntity
|
||||
from homeassistant.components.adguard.const import (
|
||||
DATA_ADGUARD_CLIENT,
|
||||
DATA_ADGUARD_VERION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import AdGuardHomeDeviceEntity
|
||||
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -22,7 +23,9 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[list[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up AdGuard Home switch based on a config entry."""
|
||||
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
|
||||
@ -49,8 +52,13 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
|
||||
"""Defines a AdGuard Home switch."""
|
||||
|
||||
def __init__(
|
||||
self, adguard, name: str, icon: str, key: str, enabled_default: bool = True
|
||||
):
|
||||
self,
|
||||
adguard: AdGuardHome,
|
||||
name: str,
|
||||
icon: str,
|
||||
key: str,
|
||||
enabled_default: bool = True,
|
||||
) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
self._state = False
|
||||
self._key = key
|
||||
@ -96,7 +104,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity):
|
||||
class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home protection switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Protection", "mdi:shield-check", "protection"
|
||||
@ -118,7 +126,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
|
||||
class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home parental control switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Parental Control", "mdi:shield-check", "parental"
|
||||
@ -140,7 +148,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
|
||||
class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home safe search switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch"
|
||||
@ -162,7 +170,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
|
||||
class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home safe search switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing"
|
||||
@ -184,7 +192,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
|
||||
class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home filtering switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering")
|
||||
|
||||
@ -204,7 +212,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
|
||||
class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch):
|
||||
"""Defines a AdGuard Home query log switch."""
|
||||
|
||||
def __init__(self, adguard) -> None:
|
||||
def __init__(self, adguard: AdGuardHome) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
|
@ -6,8 +6,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?",
|
||||
"title": "AdGuard Home \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
|
||||
"description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?",
|
||||
"title": "AdGuard Home \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?",
|
||||
"title": "AdGuard Home (complement de Hass.io)"
|
||||
"title": "AdGuard Home via complement de Hass.io"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?",
|
||||
"title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io"
|
||||
"description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed Supervisor {addon}?",
|
||||
"title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Supervisor"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -6,8 +6,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io-tilf\u00f8jelse"
|
||||
"description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Supervisor-tilf\u00f8jelsen: {addon}?",
|
||||
"title": "AdGuard Home via Supervisor-tilf\u00f8jelse"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?",
|
||||
"title": "AdGuard Home \u00fcber das Hass.io Add-on"
|
||||
"description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Supervisor-Add-On hergestellt wird: {addon}?",
|
||||
"title": "AdGuard Home \u00fcber das Supervisor Add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -6,8 +6,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Hass.io: {addon}?",
|
||||
"title": "AdGuard Home a trav\u00e9s del complemento Hass.io"
|
||||
"description": "\u00bfDesea configurar Home Assistant para conectarse a la p\u00e1gina principal de AdGuard proporcionada por el complemento Supervisor: {addon}?",
|
||||
"title": "AdGuard Home a trav\u00e9s del complemento Supervisor"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?",
|
||||
"title": "AdGuard Home a trav\u00e9s del complemento Hass.io"
|
||||
"description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?",
|
||||
"title": "AdGuard Home a trav\u00e9s del complemento Supervisor"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Kas soovid seadistada Home Assistant-i \u00fchenduse AdGuard Home'iga mida pakub Hass.io lisandmoodul: {addon} ?",
|
||||
"title": "AdGuard Home Hass.io pistikprogrammi kaudu"
|
||||
"title": "AdGuard Home Hass.io lisandmooduli abil"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -4,6 +4,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "\u05e1\u05d9\u05e1\u05de\u05d4",
|
||||
"port": "\u05e4\u05d5\u05e8\u05d8"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
@ -9,7 +12,9 @@
|
||||
"host": "Hoszt",
|
||||
"password": "Jelsz\u00f3",
|
||||
"port": "Port",
|
||||
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
|
||||
"ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
|
||||
"username": "Felhaszn\u00e1l\u00f3n\u00e9v",
|
||||
"verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "Memperbarui konfigurasi yang ada.",
|
||||
"single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Gagal terhubung"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on Supervisor {addon}?",
|
||||
"title": "AdGuard Home melalui add-on Home Assistant"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Kata sandi"
|
||||
}
|
||||
"host": "Host",
|
||||
"password": "Kata Sandi",
|
||||
"port": "Port",
|
||||
"ssl": "Menggunakan sertifikat SSL",
|
||||
"username": "Nama Pengguna",
|
||||
"verify_ssl": "Verifikasi sertifikat SSL"
|
||||
},
|
||||
"description": "Siapkan instans AdGuard Home Anda untuk pemantauan dan kontrol."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon}?",
|
||||
"description": "Vuoi configurare Home Assistant per connettersi ad AdGuard Home fornito dal componente aggiuntivo di Hass.io: {addon}?",
|
||||
"title": "AdGuard Home tramite il componente aggiuntivo di Hass.io"
|
||||
},
|
||||
"user": {
|
||||
|
@ -2,14 +2,14 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.",
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4."
|
||||
"single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
|
||||
"description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
|
||||
"title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home"
|
||||
},
|
||||
"user": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?",
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
"description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam AdGuard Home ze verbannen dee vum Supervisor add-on {addon} bereet gestallt g\u00ebtt?",
|
||||
"title": "AdGuard Home via Supervisor add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Hass.io-add-on: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
"description": "Wilt u Home Assistant configureren om verbinding te maken met AdGuard Home van de Supervisor-add-on: {addon}?",
|
||||
"title": "AdGuard Home via Supervisor add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home gitt av Hass.io-tillegg {addon}?",
|
||||
"description": "Vil du konfigurere Home Assistant for \u00e5 koble til AdGuard Home levert av Hass.io-tillegget: {addon} ?",
|
||||
"title": "AdGuard Home via Hass.io-tillegg"
|
||||
},
|
||||
"user": {
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
|
||||
"description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io: {addon}?",
|
||||
"title": "AdGuard Home przez dodatek Hass.io"
|
||||
},
|
||||
"user": {
|
||||
|
@ -6,8 +6,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Hass.io: {addon} ?",
|
||||
"title": "AdGuard Home via add-on Hass.io"
|
||||
"description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Supervisor: {addon} ?",
|
||||
"title": "AdGuard Home via add-on Supervisor"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
"title": "AdGuard Home via Supervisor add-on"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -18,7 +18,7 @@
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
|
||||
"port": "\u041f\u043e\u0440\u0442",
|
||||
"ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
|
||||
"username": "\u041b\u043e\u0433\u0438\u043d",
|
||||
"username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
|
||||
"verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL"
|
||||
},
|
||||
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home."
|
||||
|
@ -6,8 +6,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?",
|
||||
"title": "AdGuard Home preko dodatka Hass.io"
|
||||
"description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Supervisor add-on {addon} ?",
|
||||
"title": "AdGuard Home preko dodatka Supervisor"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -6,8 +6,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Hass.io Add-on: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io-till\u00e4gget"
|
||||
"description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Supervisor Add-on: {addon}?",
|
||||
"title": "AdGuard Home via Supervisor-till\u00e4gget"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?",
|
||||
"title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)"
|
||||
"description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?",
|
||||
"title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,8 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f",
|
||||
"title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 AdGuard Home"
|
||||
"description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f",
|
||||
"title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 AdGuard Home"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -2,7 +2,7 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ads
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -43,7 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities([entity])
|
||||
|
||||
|
||||
class AdsSensor(AdsEntity):
|
||||
class AdsSensor(AdsEntity, SensorEntity):
|
||||
"""Representation of an ADS sensor entity."""
|
||||
|
||||
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
|
||||
|
@ -7,24 +7,17 @@ import logging
|
||||
from advantage_air import ApiError, advantage_air
|
||||
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
||||
|
||||
ADVANTAGE_AIR_SYNC_INTERVAL = 15
|
||||
ADVANTAGE_AIR_PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"]
|
||||
PLATFORMS = ["climate", "cover", "binary_sensor", "sensor", "switch"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up Advantage Air integration."""
|
||||
hass.data[DOMAIN] = {}
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up Advantage Air config."""
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
@ -57,17 +50,15 @@ async def async_setup_entry(hass, entry):
|
||||
except ApiError as err:
|
||||
_LOGGER.warning(err)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.data:
|
||||
raise ConfigEntryNotReady
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"coordinator": coordinator,
|
||||
"async_change": async_change,
|
||||
}
|
||||
|
||||
for platform in ADVANTAGE_AIR_PLATFORMS:
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||
)
|
||||
@ -80,8 +71,8 @@ async def async_unload_entry(hass, entry):
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in ADVANTAGE_AIR_PLATFORMS
|
||||
hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Sensor platform for Advantage Air integration."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
||||
@ -40,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
)
|
||||
|
||||
|
||||
class AdvantageAirTimeTo(AdvantageAirEntity):
|
||||
class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
|
||||
"""Representation of Advantage Air timer control."""
|
||||
|
||||
def __init__(self, instance, ac_key, action):
|
||||
@ -82,7 +83,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity):
|
||||
await self.async_change({self.ac_key: {"info": {self._time_key: value}}})
|
||||
|
||||
|
||||
class AdvantageAirZoneVent(AdvantageAirEntity):
|
||||
class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
|
||||
"""Representation of Advantage Air Zone Vent Sensor."""
|
||||
|
||||
@property
|
||||
@ -115,7 +116,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity):
|
||||
return "mdi:fan-off"
|
||||
|
||||
|
||||
class AdvantageAirZoneSignal(AdvantageAirEntity):
|
||||
class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
|
||||
"""Representation of Advantage Air Zone wireless signal sensor."""
|
||||
|
||||
@property
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
|
20
homeassistant/components/advantage_air/translations/id.json
Normal file
20
homeassistant/components/advantage_air/translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,9 @@
|
||||
"data": {
|
||||
"ip_address": "IP \uc8fc\uc18c",
|
||||
"port": "\ud3ec\ud2b8"
|
||||
}
|
||||
},
|
||||
"description": "\ubcbd\uc5d0 \ubd80\ucc29\ub41c Advantage Air \ud0dc\ube14\ub9bf\uc758 API\uc5d0 \uc5f0\uacb0\ud558\uae30",
|
||||
"title": "\uc5f0\uacb0\ud558\uae30"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,18 +8,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import COMPONENTS, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR
|
||||
from .const import DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, PLATFORMS
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
"""Set up the AEMET OpenData component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Set up AEMET OpenData as config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
@ -30,16 +24,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
aemet = AEMET(api_key)
|
||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet, latitude, longitude)
|
||||
|
||||
await weather_coordinator.async_refresh()
|
||||
await weather_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
ENTRY_NAME: name,
|
||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
||||
}
|
||||
|
||||
for component in COMPONENTS:
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
||||
)
|
||||
|
||||
return True
|
||||
@ -50,8 +45,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in COMPONENTS
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Abstraction form AEMET OpenData sensors."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@ -6,7 +7,7 @@ from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT
|
||||
from .weather_update_coordinator import WeatherUpdateCoordinator
|
||||
|
||||
|
||||
class AbstractAemetSensor(CoordinatorEntity):
|
||||
class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Abstract class for an AEMET OpenData sensor."""
|
||||
|
||||
def __init__(
|
||||
@ -52,6 +53,6 @@ class AbstractAemetSensor(CoordinatorEntity):
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
|
@ -6,8 +6,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_NAME
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
|
||||
class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
@ -34,7 +34,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Powered by AEMET OpenData"
|
||||
COMPONENTS = ["sensor", "weather"]
|
||||
PLATFORMS = ["sensor", "weather"]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
|
@ -106,9 +106,10 @@ class AemetForecastSensor(AbstractAemetSensor):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
forecast = None
|
||||
forecasts = self._weather_coordinator.data.get(
|
||||
FORECAST_MODE_ATTR_API[self._forecast_mode]
|
||||
)
|
||||
if forecasts:
|
||||
return forecasts[0].get(self._sensor_type)
|
||||
return None
|
||||
forecast = forecasts[0].get(self._sensor_type)
|
||||
return forecast
|
||||
|
@ -11,8 +11,11 @@
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
}
|
||||
"longitude": "L\u00e4ngengrad",
|
||||
"name": "Name der Integration"
|
||||
},
|
||||
"description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario",
|
||||
"title": "[void]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user