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