mirror of
https://github.com/home-assistant/core.git
synced 2026-01-11 01:57:16 +01:00
Compare commits
3 Commits
2023.2.2
...
otbr_user_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18dae08244 | ||
|
|
5d21b6e7a7 | ||
|
|
eea98b22e0 |
260
.coveragerc
260
.coveragerc
File diff suppressed because it is too large
Load Diff
@@ -45,5 +45,13 @@
|
||||
"!include_dir_merge_list scalar",
|
||||
"!include_dir_merge_named scalar"
|
||||
]
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/sshd:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
DEFAULT_PYTHON: 3.9
|
||||
|
||||
jobs:
|
||||
init:
|
||||
|
||||
91
.github/workflows/ci.yaml
vendored
91
.github/workflows/ci.yaml
vendored
@@ -18,21 +18,13 @@ on:
|
||||
description: "Skip pytest"
|
||||
default: false
|
||||
type: boolean
|
||||
pylint-only:
|
||||
description: "Only run pylint"
|
||||
default: false
|
||||
type: boolean
|
||||
mypy-only:
|
||||
description: "Only run mypy"
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
PIP_CACHE_VERSION: 3
|
||||
HA_SHORT_VERSION: 2023.2
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
ALL_PYTHON_VERSIONS: "['3.10']"
|
||||
DEFAULT_PYTHON: 3.9
|
||||
ALL_PYTHON_VERSIONS: "['3.9', '3.10']"
|
||||
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
||||
PIP_CACHE: /tmp/pip-cache
|
||||
SQLALCHEMY_WARN_20: 1
|
||||
@@ -171,9 +163,6 @@ jobs:
|
||||
pre-commit:
|
||||
name: Prepare pre-commit base
|
||||
runs-on: ubuntu-20.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
needs:
|
||||
- info
|
||||
steps:
|
||||
@@ -324,62 +313,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
shopt -s globstar
|
||||
pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*}
|
||||
lint-ruff:
|
||||
name: Check ruff
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- info
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.5.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v3.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Fail job if Python cache restore failed
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore Python virtual environment from cache"
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v3.2.3
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Fail job if pre-commit cache restore failed
|
||||
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "Failed to restore pre-commit environment from cache"
|
||||
exit 1
|
||||
- name: Register ruff problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/ruff.json"
|
||||
- name: Run ruff (fully)
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual ruff --all-files
|
||||
- name: Run ruff (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
shopt -s globstar
|
||||
pre-commit run --hook-stage manual ruff --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*}
|
||||
|
||||
lint-isort:
|
||||
name: Check isort
|
||||
runs-on: ubuntu-20.04
|
||||
@@ -620,9 +554,6 @@ jobs:
|
||||
hassfest:
|
||||
name: Check hassfest
|
||||
runs-on: ubuntu-20.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -656,9 +587,6 @@ jobs:
|
||||
gen-requirements-all:
|
||||
name: Check all requirements
|
||||
runs-on: ubuntu-20.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -693,9 +621,6 @@ jobs:
|
||||
name: Check pylint
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 20
|
||||
if: |
|
||||
github.event.inputs.mypy-only != 'true'
|
||||
|| github.event.inputs.pylint-only == 'true'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -741,9 +666,6 @@ jobs:
|
||||
mypy:
|
||||
name: Check mypy
|
||||
runs-on: ubuntu-20.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
|| github.event.inputs.mypy-only == 'true'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -788,9 +710,6 @@ jobs:
|
||||
|
||||
pip-check:
|
||||
runs-on: ubuntu-20.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -831,8 +750,6 @@ jobs:
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob)
|
||||
needs:
|
||||
- info
|
||||
@@ -956,8 +873,6 @@ jobs:
|
||||
if: |
|
||||
(github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
|
||||
&& github.event.inputs.lint-only != 'true'
|
||||
&& github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& needs.info.outputs.test_full_suite == 'true'
|
||||
needs:
|
||||
- info
|
||||
|
||||
30
.github/workflows/matchers/ruff.json
vendored
30
.github/workflows/matchers/ruff.json
vendored
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "ruff-error",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"owner": "ruff-warning",
|
||||
"severity": "warning",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
DEFAULT_PYTHON: 3.9
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.231
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
stages: [manual]
|
||||
args: [--py39-plus]
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
@@ -18,9 +11,8 @@ repos:
|
||||
args:
|
||||
- --in-place
|
||||
- --remove-all-unused-imports
|
||||
stages: [manual]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@@ -49,7 +41,6 @@ repos:
|
||||
- flake8-noqa==1.3.0
|
||||
- mccabe==0.7.0
|
||||
exclude: docs/source/conf.py
|
||||
stages: [manual]
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.4
|
||||
hooks:
|
||||
@@ -60,11 +51,11 @@ repos:
|
||||
- --configfile=tests/bandit.yaml
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
rev: 5.11.4
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
stages: [manual]
|
||||
@@ -93,7 +84,7 @@ repos:
|
||||
- id: python-typing-update
|
||||
stages: [manual]
|
||||
args:
|
||||
- --py310-plus
|
||||
- --py39-plus
|
||||
- --force
|
||||
- --keep-updates
|
||||
files: ^(homeassistant|tests|script)/.+\.py$
|
||||
|
||||
@@ -112,7 +112,6 @@ homeassistant.components.fastdotcom.*
|
||||
homeassistant.components.feedreader.*
|
||||
homeassistant.components.file_upload.*
|
||||
homeassistant.components.filesize.*
|
||||
homeassistant.components.filter.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.flux_led.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
@@ -175,7 +174,6 @@ homeassistant.components.jewish_calendar.*
|
||||
homeassistant.components.kaleidescape.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
homeassistant.components.lacrosse.*
|
||||
homeassistant.components.lacrosse_view.*
|
||||
homeassistant.components.lametric.*
|
||||
homeassistant.components.laundrify.*
|
||||
@@ -203,7 +201,6 @@ homeassistant.components.mjpeg.*
|
||||
homeassistant.components.modbus.*
|
||||
homeassistant.components.modem_callerid.*
|
||||
homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.mqtt.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.nam.*
|
||||
@@ -225,7 +222,6 @@ homeassistant.components.onewire.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.openuv.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
homeassistant.components.peco.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
@@ -252,14 +248,12 @@ homeassistant.components.ridwell.*
|
||||
homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.roku.*
|
||||
homeassistant.components.rpi_power.*
|
||||
homeassistant.components.rss_feed_template.*
|
||||
homeassistant.components.rtsp_to_webrtc.*
|
||||
homeassistant.components.ruuvi_gateway.*
|
||||
homeassistant.components.ruuvitag_ble.*
|
||||
homeassistant.components.samsungtv.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.schedule.*
|
||||
homeassistant.components.scrape.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.senseme.*
|
||||
homeassistant.components.sensibo.*
|
||||
|
||||
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@@ -41,20 +41,6 @@
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Ruff",
|
||||
"type": "shell",
|
||||
"command": "pre-commit run ruff --all-files",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Pylint",
|
||||
"type": "shell",
|
||||
|
||||
24
CODEOWNERS
24
CODEOWNERS
@@ -67,6 +67,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alert/ @home-assistant/core @frenck
|
||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/almond/ @gcampax @balloob
|
||||
/tests/components/almond/ @gcampax @balloob
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
/homeassistant/components/ambiclimate/ @danielhiversen
|
||||
@@ -507,8 +509,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homematic/ @pvizeli @danielperna84
|
||||
/homeassistant/components/homewizard/ @DCSBL
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
/tests/components/honeywell/ @rdfurman @mkmer
|
||||
/homeassistant/components/honeywell/ @rdfurman
|
||||
/tests/components/honeywell/ @rdfurman
|
||||
/homeassistant/components/http/ @home-assistant/core
|
||||
/tests/components/http/ @home-assistant/core
|
||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||
@@ -738,8 +740,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/monoprice/ @etsinko @OnFreund
|
||||
/homeassistant/components/moon/ @fabaff @frenck
|
||||
/tests/components/moon/ @fabaff @frenck
|
||||
/homeassistant/components/mopeka/ @bdraco
|
||||
/tests/components/mopeka/ @bdraco
|
||||
/homeassistant/components/motion_blinds/ @starkillerOG
|
||||
/tests/components/motion_blinds/ @starkillerOG
|
||||
/homeassistant/components/motioneye/ @dermotduffy
|
||||
@@ -840,8 +840,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/onvif/ @hunterjm
|
||||
/homeassistant/components/open_meteo/ @frenck
|
||||
/tests/components/open_meteo/ @frenck
|
||||
/homeassistant/components/openai_conversation/ @balloob
|
||||
/tests/components/openai_conversation/ @balloob
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||
@@ -857,8 +855,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/openweathermap/ @fabaff @freekode @nzapponi
|
||||
/homeassistant/components/opnsense/ @mtreinish
|
||||
/tests/components/opnsense/ @mtreinish
|
||||
/homeassistant/components/oralb/ @bdraco @Lash-L
|
||||
/tests/components/oralb/ @bdraco @Lash-L
|
||||
/homeassistant/components/oralb/ @bdraco @conway20
|
||||
/tests/components/oralb/ @bdraco @conway20
|
||||
/homeassistant/components/oru/ @bvlaicu
|
||||
/homeassistant/components/otbr/ @home-assistant/core
|
||||
/tests/components/otbr/ @home-assistant/core
|
||||
@@ -896,8 +894,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/point/ @fredrike
|
||||
/homeassistant/components/poolsense/ @haemishkyd
|
||||
/tests/components/poolsense/ @haemishkyd
|
||||
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
|
||||
/homeassistant/components/powerwall/ @bdraco @jrester
|
||||
/tests/components/powerwall/ @bdraco @jrester
|
||||
/homeassistant/components/profiler/ @bdraco
|
||||
/tests/components/profiler/ @bdraco
|
||||
/homeassistant/components/progettihwsw/ @ardaseremet
|
||||
@@ -1003,8 +1001,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ruuvi_gateway/ @akx
|
||||
/homeassistant/components/ruuvitag_ble/ @akx
|
||||
/tests/components/ruuvitag_ble/ @akx
|
||||
/homeassistant/components/rympro/ @OnFreund
|
||||
/tests/components/rympro/ @OnFreund
|
||||
/homeassistant/components/sabnzbd/ @shaiu
|
||||
/tests/components/sabnzbd/ @shaiu
|
||||
/homeassistant/components/safe_mode/ @home-assistant/core
|
||||
@@ -1143,8 +1139,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/stiebel_eltron/ @fucm
|
||||
/homeassistant/components/stookalert/ @fwestenberg @frenck
|
||||
/tests/components/stookalert/ @fwestenberg @frenck
|
||||
/homeassistant/components/stookwijzer/ @fwestenberg
|
||||
/tests/components/stookwijzer/ @fwestenberg
|
||||
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter
|
||||
/tests/components/stream/ @hunterjm @uvjustin @allenporter
|
||||
/homeassistant/components/stt/ @pvizeli
|
||||
@@ -1206,8 +1200,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/thermopro/ @bdraco
|
||||
/tests/components/thermopro/ @bdraco
|
||||
/homeassistant/components/thethingsnetwork/ @fabaff
|
||||
/homeassistant/components/thread/ @home-assistant/core
|
||||
/tests/components/thread/ @home-assistant/core
|
||||
/homeassistant/components/threshold/ @fabaff
|
||||
/tests/components/threshold/ @fabaff
|
||||
/homeassistant/components/tibber/ @danielhiversen
|
||||
|
||||
@@ -5,8 +5,6 @@ FROM ${BUILD_FROM}
|
||||
ENV \
|
||||
S6_SERVICES_GRACETIME=220000
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
|
||||
22
codecov.yml
22
codecov.yml
@@ -6,37 +6,19 @@ coverage:
|
||||
default:
|
||||
target: 90
|
||||
threshold: 0.09
|
||||
required:
|
||||
config-flows:
|
||||
target: auto
|
||||
threshold: 1
|
||||
paths:
|
||||
- homeassistant/components/*/config_flow.py
|
||||
- homeassistant/components/*/device_action.py
|
||||
- homeassistant/components/*/device_condition.py
|
||||
- homeassistant/components/*/device_trigger.py
|
||||
- homeassistant/components/*/diagnostics.py
|
||||
- homeassistant/components/*/group.py
|
||||
- homeassistant/components/*/intent.py
|
||||
- homeassistant/components/*/logbook.py
|
||||
- homeassistant/components/*/media_source.py
|
||||
- homeassistant/components/*/scene.py
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
required:
|
||||
config-flows:
|
||||
target: 100
|
||||
threshold: 0
|
||||
paths:
|
||||
- homeassistant/components/*/config_flow.py
|
||||
- homeassistant/components/*/device_action.py
|
||||
- homeassistant/components/*/device_condition.py
|
||||
- homeassistant/components/*/device_trigger.py
|
||||
- homeassistant/components/*/diagnostics.py
|
||||
- homeassistant/components/*/group.py
|
||||
- homeassistant/components/*/intent.py
|
||||
- homeassistant/components/*/logbook.py
|
||||
- homeassistant/components/*/media_source.py
|
||||
- homeassistant/components/*/scene.py
|
||||
comment: false
|
||||
|
||||
# To make partial tests possible,
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Home Assistant documentation build configuration file.
|
||||
|
||||
This file is execfile()d with the current directory set to its
|
||||
containing dir.
|
||||
|
||||
Note that not all possible configuration values are present in this
|
||||
autogenerated file.
|
||||
|
||||
All configuration values have a default; values that are commented out
|
||||
serve to show the default.
|
||||
|
||||
If extensions (or modules to document with autodoc) are in another directory,
|
||||
add these directories to sys.path here. If the directory is relative to the
|
||||
documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
"""
|
||||
#
|
||||
# Home-Assistant documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Aug 28 13:13:10 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
@@ -109,17 +110,17 @@ def linkcode_resolve(domain, info):
|
||||
for part in fullname.split("."):
|
||||
try:
|
||||
obj = getattr(obj, part)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except:
|
||||
return None
|
||||
try:
|
||||
fn = inspect.getsourcefile(obj)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except:
|
||||
fn = None
|
||||
if not fn:
|
||||
return None
|
||||
try:
|
||||
source, lineno = inspect.findsource(obj)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except:
|
||||
lineno = None
|
||||
if lineno:
|
||||
linespec = "#L%d" % (lineno + 1)
|
||||
|
||||
@@ -35,7 +35,7 @@ def validate_python() -> None:
|
||||
|
||||
def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from . import config as config_util
|
||||
|
||||
lib_dir = os.path.join(config_dir, "deps")
|
||||
@@ -77,7 +77,7 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from . import config as config_util
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -184,7 +184,7 @@ def main() -> int:
|
||||
validate_os()
|
||||
|
||||
if args.script is not None:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from . import scripts
|
||||
|
||||
return scripts.run(args.script)
|
||||
@@ -192,7 +192,7 @@ def main() -> int:
|
||||
config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config))
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from . import runner
|
||||
|
||||
runtime_conf = runner.RuntimeConfig(
|
||||
|
||||
@@ -5,7 +5,7 @@ import asyncio
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
from typing import Any, cast
|
||||
from typing import Any, Optional, cast
|
||||
|
||||
import jwt
|
||||
|
||||
@@ -24,7 +24,7 @@ EVENT_USER_UPDATED = "user_updated"
|
||||
EVENT_USER_REMOVED = "user_removed"
|
||||
|
||||
_MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||
_ProviderKey = tuple[str, str | None]
|
||||
_ProviderKey = tuple[str, Optional[str]]
|
||||
_ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
"""Common code for permissions."""
|
||||
from collections.abc import Mapping
|
||||
from typing import Union
|
||||
|
||||
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
||||
|
||||
ValueType = (
|
||||
ValueType = Union[
|
||||
# Example: entities.all = { read: true, control: true }
|
||||
Mapping[str, bool]
|
||||
| bool
|
||||
| None
|
||||
)
|
||||
Mapping[str, bool],
|
||||
bool,
|
||||
None,
|
||||
]
|
||||
|
||||
# Example: entities.domains = { light: … }
|
||||
SubCategoryDict = Mapping[str, ValueType]
|
||||
|
||||
SubCategoryType = SubCategoryDict | bool | None
|
||||
SubCategoryType = Union[SubCategoryDict, bool, None]
|
||||
|
||||
CategoryType = (
|
||||
CategoryType = Union[
|
||||
# Example: entities.domains
|
||||
Mapping[str, SubCategoryType]
|
||||
Mapping[str, SubCategoryType],
|
||||
# Example: entities.all
|
||||
| Mapping[str, ValueType]
|
||||
| bool
|
||||
| None
|
||||
)
|
||||
Mapping[str, ValueType],
|
||||
bool,
|
||||
None,
|
||||
]
|
||||
|
||||
# Example: { entities: … }
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
|
||||
@@ -3,13 +3,13 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import cast
|
||||
from typing import Optional, cast
|
||||
|
||||
from .const import SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
||||
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]]
|
||||
SubCatLookupType = dict[str, LookupFunc]
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from ipaddress import (
|
||||
ip_address,
|
||||
ip_network,
|
||||
)
|
||||
from typing import Any, cast
|
||||
from typing import Any, Union, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -27,8 +27,8 @@ from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from .. import InvalidAuthError
|
||||
from ..models import Credentials, RefreshToken, UserMeta
|
||||
|
||||
IPAddress = IPv4Address | IPv6Address
|
||||
IPNetwork = IPv4Network | IPv6Network
|
||||
IPAddress = Union[IPv4Address, IPv6Address]
|
||||
IPNetwork = Union[IPv4Network, IPv6Network]
|
||||
|
||||
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
||||
CONF_TRUSTED_USERS = "trusted_users"
|
||||
|
||||
@@ -346,7 +346,7 @@ def async_enable_logging(
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from colorlog import ColoredFormatter
|
||||
|
||||
# basicConfig must be called after importing colorlog in order to
|
||||
@@ -406,6 +406,7 @@ def async_enable_logging(
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||
):
|
||||
|
||||
err_handler: (
|
||||
logging.handlers.RotatingFileHandler
|
||||
| logging.handlers.TimedRotatingFileHandler
|
||||
|
||||
@@ -3,14 +3,10 @@ from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from jaraco.abode.automation import Automation as AbodeAuto
|
||||
from jaraco.abode.client import Client as Abode
|
||||
from jaraco.abode.devices.base import Device as AbodeDev
|
||||
from jaraco.abode.exceptions import (
|
||||
AuthenticationException as AbodeAuthenticationException,
|
||||
Exception as AbodeException,
|
||||
)
|
||||
from jaraco.abode.helpers.timeline import Groups as GROUPS
|
||||
from abodepy import Abode, AbodeAutomation as AbodeAuto
|
||||
from abodepy.devices import AbodeDevice as AbodeDev
|
||||
from abodepy.exceptions import AbodeAuthenticationException, AbodeException
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -30,7 +26,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, entity
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
|
||||
from .const import ATTRIBUTION, CONF_POLLING, DEFAULT_CACHEDB, DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
@@ -86,6 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
polling = entry.data[CONF_POLLING]
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
|
||||
# For previous config entries where unique_id is None
|
||||
if entry.unique_id is None:
|
||||
@@ -95,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
try:
|
||||
abode = await hass.async_add_executor_job(
|
||||
Abode, username, password, True, True, True
|
||||
Abode, username, password, True, True, True, cache
|
||||
)
|
||||
|
||||
except AbodeAuthenticationException as ex:
|
||||
@@ -228,17 +225,17 @@ def setup_abode_events(hass: HomeAssistant) -> None:
|
||||
hass.bus.fire(event, data)
|
||||
|
||||
events = [
|
||||
GROUPS.ALARM,
|
||||
GROUPS.ALARM_END,
|
||||
GROUPS.PANEL_FAULT,
|
||||
GROUPS.PANEL_RESTORE,
|
||||
GROUPS.AUTOMATION,
|
||||
GROUPS.DISARM,
|
||||
GROUPS.ARM,
|
||||
GROUPS.ARM_FAULT,
|
||||
GROUPS.TEST,
|
||||
GROUPS.CAPTURE,
|
||||
GROUPS.DEVICE,
|
||||
TIMELINE.ALARM_GROUP,
|
||||
TIMELINE.ALARM_END_GROUP,
|
||||
TIMELINE.PANEL_FAULT_GROUP,
|
||||
TIMELINE.PANEL_RESTORE_GROUP,
|
||||
TIMELINE.AUTOMATION_GROUP,
|
||||
TIMELINE.DISARM_GROUP,
|
||||
TIMELINE.ARM_GROUP,
|
||||
TIMELINE.ARM_FAULT_GROUP,
|
||||
TIMELINE.TEST_GROUP,
|
||||
TIMELINE.CAPTURE_GROUP,
|
||||
TIMELINE.DEVICE_GROUP,
|
||||
]
|
||||
|
||||
for event in events:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for Abode Security System alarm control panels."""
|
||||
from __future__ import annotations
|
||||
|
||||
from jaraco.abode.devices.alarm import Alarm as AbodeAl
|
||||
from abodepy.devices.alarm import AbodeAlarm as AbodeAl
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature
|
||||
|
||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
from contextlib import suppress
|
||||
from typing import cast
|
||||
|
||||
from jaraco.abode.devices.sensor import BinarySensor as ABBinarySensor
|
||||
from jaraco.abode.helpers import constants as CONST
|
||||
from abodepy.devices.binary_sensor import AbodeBinarySensor as ABBinarySensor
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from jaraco.abode.devices.base import Device as AbodeDev
|
||||
from jaraco.abode.devices.camera import Camera as AbodeCam
|
||||
from jaraco.abode.helpers import constants as CONST, timeline as TIMELINE
|
||||
from abodepy.devices import CONST, AbodeDevice as AbodeDev
|
||||
from abodepy.devices.camera import AbodeCamera as AbodeCam
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
import requests
|
||||
from requests.models import Response
|
||||
|
||||
@@ -30,7 +30,7 @@ async def async_setup_entry(
|
||||
data: AbodeSystem = hass.data[DOMAIN]
|
||||
|
||||
async_add_entities(
|
||||
AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) # pylint: disable=no-member
|
||||
AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)
|
||||
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA)
|
||||
)
|
||||
|
||||
|
||||
@@ -5,12 +5,9 @@ from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
from typing import Any, cast
|
||||
|
||||
from jaraco.abode.client import Client as Abode
|
||||
from jaraco.abode.exceptions import (
|
||||
AuthenticationException as AbodeAuthenticationException,
|
||||
Exception as AbodeException,
|
||||
)
|
||||
from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED
|
||||
from abodepy import Abode
|
||||
from abodepy.exceptions import AbodeAuthenticationException, AbodeException
|
||||
from abodepy.helpers.errors import MFA_CODE_REQUIRED
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -18,7 +15,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
from .const import CONF_POLLING, DEFAULT_CACHEDB, DOMAIN, LOGGER
|
||||
|
||||
CONF_MFA = "mfa_code"
|
||||
|
||||
@@ -38,6 +35,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_MFA): str,
|
||||
}
|
||||
|
||||
self._cache: str | None = None
|
||||
self._mfa_code: str | None = None
|
||||
self._password: str | None = None
|
||||
self._polling: bool = False
|
||||
@@ -45,11 +43,12 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def _async_abode_login(self, step_id: str) -> FlowResult:
|
||||
"""Handle login with Abode."""
|
||||
self._cache = self.hass.config.path(DEFAULT_CACHEDB)
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
Abode, self._username, self._password, True, False, False
|
||||
Abode, self._username, self._password, True, False, False, self._cache
|
||||
)
|
||||
|
||||
except AbodeException as ex:
|
||||
@@ -78,7 +77,12 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle multi-factor authentication (MFA) login with Abode."""
|
||||
try:
|
||||
# Create instance to access login method for passing MFA code
|
||||
abode = Abode(auto_login=False, get_devices=False, get_automations=False)
|
||||
abode = Abode(
|
||||
auto_login=False,
|
||||
get_devices=False,
|
||||
get_automations=False,
|
||||
cache_path=self._cache,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
abode.login, self._username, self._password, self._mfa_code
|
||||
)
|
||||
|
||||
@@ -6,4 +6,5 @@ LOGGER = logging.getLogger(__package__)
|
||||
DOMAIN = "abode"
|
||||
ATTRIBUTION = "Data provided by goabode.com"
|
||||
|
||||
DEFAULT_CACHEDB = "abodepy_cache.pickle"
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Support for Abode Security System covers."""
|
||||
from typing import Any
|
||||
|
||||
from jaraco.abode.devices.cover import Cover as AbodeCV
|
||||
from jaraco.abode.helpers import constants as CONST
|
||||
from abodepy.devices.cover import AbodeCover as AbodeCV
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.cover import CoverEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from jaraco.abode.devices.light import Light as AbodeLT
|
||||
from jaraco.abode.helpers import constants as CONST
|
||||
from abodepy.devices.light import AbodeLight as AbodeLT
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Support for the Abode Security System locks."""
|
||||
from typing import Any
|
||||
|
||||
from jaraco.abode.devices.lock import Lock as AbodeLK
|
||||
from jaraco.abode.helpers import constants as CONST
|
||||
from abodepy.devices.lock import AbodeLock as AbodeLK
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"name": "Abode",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||
"requirements": ["jaraco.abode==3.2.1"],
|
||||
"requirements": ["abodepy==1.2.0"],
|
||||
"codeowners": ["@shred86"],
|
||||
"homekit": {
|
||||
"models": ["Abode", "Iota"]
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"]
|
||||
"loggers": ["abodepy", "lomond"]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from jaraco.abode.devices.sensor import Sensor as AbodeSense
|
||||
from jaraco.abode.helpers import constants as CONST
|
||||
from abodepy.devices.sensor import CONST, AbodeSensor as AbodeSense
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
||||
@@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from jaraco.abode.devices.switch import Switch as AbodeSW
|
||||
from jaraco.abode.helpers import constants as CONST
|
||||
from abodepy.devices.switch import CONST, AbodeSwitch as AbodeSW
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -17,10 +17,10 @@ from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UV_INDEX,
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolumetricFlux,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -290,11 +290,11 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Precipitation",
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
name="Precipitation",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
metric_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
|
||||
us_customary_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR,
|
||||
metric_unit=UnitOfPrecipitationDepth.MILLIMETERS,
|
||||
us_customary_unit=UnitOfPrecipitationDepth.INCHES,
|
||||
value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]),
|
||||
attr_fn=lambda data: {"type": data["PrecipitationType"]},
|
||||
),
|
||||
@@ -452,7 +452,7 @@ def _get_sensor_data(
|
||||
return sensors[ATTR_FORECAST][forecast_day][kind]
|
||||
|
||||
if kind == "Precipitation":
|
||||
return sensors["PrecipitationSummary"]["PastHour"]
|
||||
return sensors["PrecipitationSummary"][kind]
|
||||
|
||||
return sensors[kind]
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
# convert title and unique_id to string
|
||||
if config_entry.version == 1:
|
||||
if isinstance(config_entry.unique_id, int):
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
unique_id=str(config_entry.unique_id),
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
},
|
||||
"step": {
|
||||
"cloud": {
|
||||
"data": {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP-adress",
|
||||
"password": "L\u00f6senord"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,12 +266,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
"import_source": source,
|
||||
CONF_API_KEY: entry.data[CONF_API_KEY],
|
||||
**geography,
|
||||
},
|
||||
context={"source": source},
|
||||
data={CONF_API_KEY: entry.data[CONF_API_KEY], **geography},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -171,13 +171,6 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Define the config flow to handle options."""
|
||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, str]) -> FlowResult:
|
||||
"""Handle import of config entry version 1 data."""
|
||||
import_source = import_data.pop("import_source")
|
||||
if import_source == "geography_by_coords":
|
||||
return await self.async_step_geography_by_coords(import_data)
|
||||
return await self.async_step_geography_by_name(import_data)
|
||||
|
||||
async def async_step_geography_by_coords(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
"country": "\u0421\u0442\u0440\u0430\u043d\u0430"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u0425\u043e\u0441\u0442",
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u0430"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API \u043a\u043b\u044e\u0447"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds",
|
||||
"title": "Configura una ubicaci\u00f3 geogr\u00e0fica"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Amfitri\u00f3",
|
||||
"password": "Contrasenya"
|
||||
},
|
||||
"description": "Monitoritza una unitat personal d'AirVisual. Pots obtenir la contrasenya des de la interf\u00edcie d'usuari (UI) de la unitat.",
|
||||
"title": "Configuraci\u00f3 d'AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Clau API"
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
"country": "Zem\u011b"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Hostitel",
|
||||
"password": "Heslo"
|
||||
},
|
||||
"title": "Nastaven\u00ed AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Kl\u00ed\u010d API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.",
|
||||
"title": "Konfiguriere einen Standort"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Passwort"
|
||||
},
|
||||
"description": "\u00dcberwache eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.",
|
||||
"title": "Konfiguriere einen AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-Schl\u00fcssel"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf AirVisual cloud API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03c0\u03bf\u03bb\u03b9\u03c4\u03b5\u03af\u03b1/\u03c7\u03ce\u03c1\u03b1.",
|
||||
"title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03af\u03b1\u03c2"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2",
|
||||
"password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2"
|
||||
},
|
||||
"description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03ae \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 AirVisual. \u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf UI \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2.",
|
||||
"title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
|
||||
"title": "Configure a Geography"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Password"
|
||||
},
|
||||
"description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.",
|
||||
"title": "Configure an AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API Key"
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
"description": "Utilice la API en la nube de AirVisual para monitorear una ciudad/estado/pa\u00eds.",
|
||||
"title": "Configurar una geograf\u00eda"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Direcci\u00f3n IP/nombre de host de la unidad",
|
||||
"password": "Contrase\u00f1a de la unidad"
|
||||
},
|
||||
"description": "Monitoree una unidad AirVisual personal. La contrase\u00f1a se puede recuperar de la interfaz de usuario de la unidad.",
|
||||
"title": "Configurar un AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Vuelva a autenticar AirVisual"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Usar la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.",
|
||||
"title": "Configurar una geograf\u00eda"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Contrase\u00f1a"
|
||||
},
|
||||
"description": "Supervisar una unidad AirVisual personal. La contrase\u00f1a se puede recuperar desde la IU de la unidad.",
|
||||
"title": "Configurar un AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Clave API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.",
|
||||
"title": "Seadista Geography sidumine"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u00dcksuse IP-aadress / hostinimi",
|
||||
"password": "Salas\u00f5na"
|
||||
},
|
||||
"description": "J\u00e4lgige isiklikku AirVisual-seadet. Parooli saab hankida seadme kasutajaliidese kaudu.",
|
||||
"title": "Seadistage AirVisual Node / Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API v\u00f5ti"
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"config": {
|
||||
"error": {
|
||||
"general_error": "Tapahtui tuntematon virhe."
|
||||
},
|
||||
"step": {
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"password": "Salasana"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.",
|
||||
"title": "Configurer un lieu g\u00e9ographique"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "H\u00f4te",
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.",
|
||||
"title": "Configurer un noeud AirVisual Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Cl\u00e9 d'API"
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
"api_key": "\u05de\u05e4\u05ea\u05d7 API"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u05de\u05d0\u05e8\u05d7",
|
||||
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
|
||||
},
|
||||
"description": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d9\u05d7\u05d9\u05d3\u05ea AirVisual \u05d0\u05d9\u05e9\u05d9\u05ea. \u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05de\u05e9\u05e7 \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4."
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "\u05de\u05e4\u05ea\u05d7 API"
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
"config": {
|
||||
"error": {
|
||||
"general_error": "\u0915\u094b\u0908 \u0905\u091c\u094d\u091e\u093e\u0924 \u0924\u094d\u0930\u0941\u091f\u093f \u0925\u0940\u0964"
|
||||
},
|
||||
"step": {
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u0907\u0915\u093e\u0908 \u0915\u0947 \u0906\u0908\u092a\u0940 \u092a\u0924\u0947/\u0939\u094b\u0938\u094d\u091f\u0928\u093e\u092e",
|
||||
"password": "\u0907\u0915\u093e\u0908 \u092a\u093e\u0938\u0935\u0930\u094d\u0921"
|
||||
},
|
||||
"description": "\u090f\u0915 \u0935\u094d\u092f\u0915\u094d\u0924\u093f\u0917\u0924 \u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0907\u0915\u093e\u0908 \u0915\u0940 \u0928\u093f\u0917\u0930\u093e\u0928\u0940 \u0915\u0930\u0947\u0902\u0964 \u092a\u093e\u0938\u0935\u0930\u094d\u0921 \u092f\u0942\u0928\u093f\u091f \u0915\u0947 \u092f\u0942\u0906\u0908 \u0938\u0947 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u093f\u092f\u093e \u091c\u093e \u0938\u0915\u0924\u093e \u0939\u0948\u0964",
|
||||
"title": "\u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0928\u094b\u0921 \u092a\u094d\u0930\u094b"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t egy v\u00e1ros / \u00e1llam / orsz\u00e1g figyel\u00e9s\u00e9hez.",
|
||||
"title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "C\u00edm",
|
||||
"password": "Jelsz\u00f3"
|
||||
},
|
||||
"description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.",
|
||||
"title": "AirVisual Node/Pro konfigur\u00e1l\u00e1sa"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API kulcs"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Gunakan API cloud AirVisual untuk memantau kota/negara bagian/negara.",
|
||||
"title": "Konfigurasikan Lokasi Geografi"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Kata Sandi"
|
||||
},
|
||||
"description": "Pantau unit AirVisual pribadi. Kata sandi dapat diambil dari antarmuka unit.",
|
||||
"title": "Konfigurasikan AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Kunci API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.",
|
||||
"title": "Configura un'area geografica"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Password"
|
||||
},
|
||||
"description": "Monitora un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.",
|
||||
"title": "Configura un AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Chiave API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u90fd\u5e02/\u5dde/\u56fd\u3092\u76e3\u8996\u3057\u307e\u3059\u3002",
|
||||
"title": "Geography\u306e\u8a2d\u5b9a"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u30db\u30b9\u30c8",
|
||||
"password": "\u30d1\u30b9\u30ef\u30fc\u30c9"
|
||||
},
|
||||
"description": "\u500b\u4eba\u306eAirVisual\u30e6\u30cb\u30c3\u30c8\u3092\u76e3\u8996\u3057\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u672c\u4f53\u306eUI\u304b\u3089\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002",
|
||||
"title": "AirVisual Node/Pro\u306e\u8a2d\u5b9a"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API\u30ad\u30fc"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub3c4\uc2dc/\uc8fc/\uad6d\uac00\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.",
|
||||
"title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\ud638\uc2a4\ud2b8",
|
||||
"password": "\ube44\ubc00\ubc88\ud638"
|
||||
},
|
||||
"description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
|
||||
"title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API \ud0a4"
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
"state": "Kanton"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Passwuert"
|
||||
},
|
||||
"description": "Pers\u00e9inlech Airvisual Unit\u00e9it iwwerwaachen. Passwuert kann vum UI vum Apparat ausgelies ginn.",
|
||||
"title": "Airvisual Node/Pro ariichten"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API Schl\u00ebssel"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"name": "\u041d\u0430\u0437\u0432\u0430"
|
||||
"password": "Slapta\u017eodis"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.",
|
||||
"title": "Configureer een geografie"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Host",
|
||||
"password": "Wachtwoord"
|
||||
},
|
||||
"description": "Monitor een persoonlijke AirVisual-eenheid. Het wachtwoord kan worden opgehaald uit de gebruikersinterface van het apparaat.",
|
||||
"title": "Configureer een AirVisual Node / Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-sleutel"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.",
|
||||
"title": "Konfigurer en Geography"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Vert",
|
||||
"password": "Passord"
|
||||
},
|
||||
"description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.",
|
||||
"title": "Konfigurer en AirVisual Node / Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-n\u00f8kkel"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.",
|
||||
"title": "Konfiguracja Geography"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Nazwa hosta lub adres IP",
|
||||
"password": "Has\u0142o"
|
||||
},
|
||||
"description": "Monitoruj jednostk\u0119 AirVisual. Has\u0142o mo\u017cna odzyska\u0107 z interfejsu u\u017cytkownika urz\u0105dzenia.",
|
||||
"title": "Konfiguracja AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Klucz API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Use a API de nuvem AirVisual para monitorar uma cidade/estado/pa\u00eds.",
|
||||
"title": "Configurar uma geografia"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Nome do host",
|
||||
"password": "Senha"
|
||||
},
|
||||
"description": "Monitore uma unidade AirVisual pessoal. A senha pode ser recuperada da interface do usu\u00e1rio da unidade.",
|
||||
"title": "Configurar um n\u00f3/pro AirVisual"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Chave da API"
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
"latitude": "Latitude"
|
||||
}
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Endere\u00e7o",
|
||||
"password": "Palavra-passe"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "Chave da API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u0425\u043e\u0441\u0442",
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
|
||||
},
|
||||
"description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AirVisual Node / Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Pou\u017eite cloudov\u00e9 API AirVisual na monitorovanie mesta/\u0161t\u00e1tu/krajiny.",
|
||||
"title": "Konfigur\u00e1cia geografie"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Hostite\u013e",
|
||||
"password": "Heslo"
|
||||
},
|
||||
"description": "Monitorujte osobn\u00fa jednotku AirVisual. Heslo je mo\u017en\u00e9 z\u00edska\u0165 z pou\u017e\u00edvate\u013esk\u00e9ho rozhrania jednotky.",
|
||||
"title": "Nastavenie AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API k\u013e\u00fa\u010d"
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
"invalid_api_key": "Vpisan neveljaven API klju\u010d"
|
||||
},
|
||||
"step": {
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "IP naslov/ime gostitelja enote",
|
||||
"password": "Geslo enote"
|
||||
},
|
||||
"description": "Spremljajte osebno napravo AirVisual. Geslo je mogo\u010de pridobiti iz uporabni\u0161kega vmesnika enote.",
|
||||
"title": "Konfigurirajte AirVisual Node/Pro"
|
||||
},
|
||||
"user": {
|
||||
"description": "Spremljajte kakovost zraka na zemljepisni lokaciji.",
|
||||
"title": "Nastavite AirVisual"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Anv\u00e4nd AirVisuals moln-API f\u00f6r att \u00f6vervaka en stad/stat/land.",
|
||||
"title": "Konfigurera en geografi"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Enhets IP-adress / v\u00e4rdnamn",
|
||||
"password": "Enhetsl\u00f6senord"
|
||||
},
|
||||
"description": "\u00d6vervaka en personlig AirVisual-enhet. L\u00f6senordet kan h\u00e4mtas fr\u00e5n enhetens anv\u00e4ndargr\u00e4nssnitt.",
|
||||
"title": "Konfigurera en AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-nyckel"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Bir \u015fehri/eyalet/\u00fclkeyi izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.",
|
||||
"title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "Sunucu",
|
||||
"password": "Parola"
|
||||
},
|
||||
"description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir.",
|
||||
"title": "Bir AirVisual Node/Pro'yu yap\u0131land\u0131r\u0131n"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API Anahtar\u0131"
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
"invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
|
||||
},
|
||||
"step": {
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u0425\u043e\u0441\u0442",
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
|
||||
},
|
||||
"description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.",
|
||||
"title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002",
|
||||
"title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19"
|
||||
},
|
||||
"node_pro": {
|
||||
"data": {
|
||||
"ip_address": "\u4e3b\u6a5f\u7aef",
|
||||
"password": "\u5bc6\u78bc"
|
||||
},
|
||||
"description": "\u76e3\u63a7\u500b\u4eba AirVisual \u88dd\u7f6e\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u88dd\u7f6e UI \u7372\u5f97\u3002",
|
||||
"title": "\u8a2d\u5b9a AirVisual Node/Pro"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API \u91d1\u9470"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,5 +152,5 @@ class AirzoneZoneEntity(AirzoneEntity):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to set zone {self.name}: {error}"
|
||||
) from error
|
||||
|
||||
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
|
||||
else:
|
||||
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
else:
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry,
|
||||
data={
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.55"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.52"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,6 +270,7 @@ class Alert(Entity):
|
||||
await self._send_notification_message(message)
|
||||
|
||||
async def _send_notification_message(self, message: Any) -> None:
|
||||
|
||||
if not self._notifiers:
|
||||
return
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ class Auth:
|
||||
return dt.utcnow() < preemptive_expire_time
|
||||
|
||||
async def _async_request_new_token(self, lwa_params):
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(10):
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.components import (
|
||||
input_number,
|
||||
light,
|
||||
media_player,
|
||||
number,
|
||||
timer,
|
||||
vacuum,
|
||||
)
|
||||
@@ -27,7 +26,6 @@ from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
PERCENTAGE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
@@ -43,10 +41,6 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_UNLOCKED,
|
||||
STATE_UNLOCKING,
|
||||
UnitOfLength,
|
||||
UnitOfMass,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import State
|
||||
import homeassistant.util.color as color_util
|
||||
@@ -71,34 +65,6 @@ from .resources import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UNIT_TO_CATALOG_TAG = {
|
||||
UnitOfTemperature.CELSIUS: AlexaGlobalCatalog.UNIT_TEMPERATURE_CELSIUS,
|
||||
UnitOfTemperature.FAHRENHEIT: AlexaGlobalCatalog.UNIT_TEMPERATURE_FAHRENHEIT,
|
||||
UnitOfTemperature.KELVIN: AlexaGlobalCatalog.UNIT_TEMPERATURE_KELVIN,
|
||||
UnitOfLength.METERS: AlexaGlobalCatalog.UNIT_DISTANCE_METERS,
|
||||
UnitOfLength.KILOMETERS: AlexaGlobalCatalog.UNIT_DISTANCE_KILOMETERS,
|
||||
UnitOfLength.INCHES: AlexaGlobalCatalog.UNIT_DISTANCE_INCHES,
|
||||
UnitOfLength.FEET: AlexaGlobalCatalog.UNIT_DISTANCE_FEET,
|
||||
UnitOfLength.YARDS: AlexaGlobalCatalog.UNIT_DISTANCE_YARDS,
|
||||
UnitOfLength.MILES: AlexaGlobalCatalog.UNIT_DISTANCE_MILES,
|
||||
UnitOfMass.GRAMS: AlexaGlobalCatalog.UNIT_MASS_GRAMS,
|
||||
UnitOfMass.KILOGRAMS: AlexaGlobalCatalog.UNIT_MASS_KILOGRAMS,
|
||||
UnitOfMass.POUNDS: AlexaGlobalCatalog.UNIT_WEIGHT_POUNDS,
|
||||
UnitOfMass.OUNCES: AlexaGlobalCatalog.UNIT_WEIGHT_OUNCES,
|
||||
UnitOfVolume.LITERS: AlexaGlobalCatalog.UNIT_VOLUME_LITERS,
|
||||
UnitOfVolume.CUBIC_FEET: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_METERS,
|
||||
UnitOfVolume.GALLONS: AlexaGlobalCatalog.UNIT_VOLUME_GALLONS,
|
||||
PERCENTAGE: AlexaGlobalCatalog.UNIT_PERCENT,
|
||||
"preset": AlexaGlobalCatalog.SETTING_PRESET,
|
||||
}
|
||||
|
||||
|
||||
def get_resource_by_unit_of_measurement(entity: State) -> str:
|
||||
"""Translate the unit of measurement to an Alexa Global Catalog keyword."""
|
||||
unit: str = entity.attributes.get("unit_of_measurement", "preset")
|
||||
return UNIT_TO_CATALOG_TAG.get(unit, AlexaGlobalCatalog.SETTING_PRESET)
|
||||
|
||||
|
||||
class AlexaCapability:
|
||||
"""Base class for Alexa capability interfaces.
|
||||
@@ -112,16 +78,10 @@ class AlexaCapability:
|
||||
|
||||
supported_locales = {"en-US"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entity: State,
|
||||
instance: str | None = None,
|
||||
non_controllable_properties: bool | None = None,
|
||||
) -> None:
|
||||
def __init__(self, entity: State, instance: str | None = None) -> None:
|
||||
"""Initialize an Alexa capability."""
|
||||
self.entity = entity
|
||||
self.instance = instance
|
||||
self._non_controllable_properties = non_controllable_properties
|
||||
|
||||
def name(self) -> str:
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@@ -141,7 +101,7 @@ class AlexaCapability:
|
||||
|
||||
def properties_non_controllable(self) -> bool | None:
|
||||
"""Return True if non controllable."""
|
||||
return self._non_controllable_properties
|
||||
return None
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property.
|
||||
@@ -175,16 +135,16 @@ class AlexaCapability:
|
||||
def configuration(self):
|
||||
"""Return the configuration object.
|
||||
|
||||
Applicable to the ThermostatController, SecurityControlPanel, ModeController,
|
||||
RangeController, and EventDetectionSensor.
|
||||
Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController,
|
||||
and EventDetectionSensor.
|
||||
"""
|
||||
return []
|
||||
|
||||
def configurations(self):
|
||||
"""Return the configurations object.
|
||||
|
||||
The plural configurations object is different that the singular configuration
|
||||
object. Applicable to EqualizerController interface.
|
||||
The plural configurations object is different that the singular configuration object.
|
||||
Applicable to EqualizerController interface.
|
||||
"""
|
||||
return []
|
||||
|
||||
@@ -236,8 +196,7 @@ class AlexaCapability:
|
||||
if configuration := self.configuration():
|
||||
result["configuration"] = configuration
|
||||
|
||||
# The plural configurations object is different than the singular
|
||||
# configuration object above.
|
||||
# The plural configurations object is different than the singular configuration object above.
|
||||
if configurations := self.configurations():
|
||||
result["configurations"] = configurations
|
||||
|
||||
@@ -798,8 +757,7 @@ class AlexaPlaybackController(AlexaCapability):
|
||||
def supported_operations(self):
|
||||
"""Return the supportedOperations object.
|
||||
|
||||
Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind,
|
||||
StartOver, Stop
|
||||
Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop
|
||||
"""
|
||||
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
@@ -1159,9 +1117,7 @@ class AlexaThermostatController(AlexaCapability):
|
||||
def configuration(self):
|
||||
"""Return configuration object.
|
||||
|
||||
Translates climate HVAC_MODES and PRESETS to supported Alexa
|
||||
ThermostatMode Values.
|
||||
|
||||
Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values.
|
||||
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
|
||||
"""
|
||||
supported_modes = []
|
||||
@@ -1177,8 +1133,7 @@ class AlexaThermostatController(AlexaCapability):
|
||||
if thermostat_mode:
|
||||
supported_modes.append(thermostat_mode)
|
||||
|
||||
# Return False for supportsScheduling until supported with event
|
||||
# listener in handler.
|
||||
# Return False for supportsScheduling until supported with event listener in handler.
|
||||
configuration = {"supportsScheduling": False}
|
||||
|
||||
if supported_modes:
|
||||
@@ -1315,15 +1270,12 @@ class AlexaSecurityPanelController(AlexaCapability):
|
||||
class AlexaModeController(AlexaCapability):
|
||||
"""Implements Alexa.ModeController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController,
|
||||
ToggleController within the same device.
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property should be a concatenated string of device domain period
|
||||
and single word. e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property
|
||||
strings within the same device. e.g. Instance property cover.position &
|
||||
cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
@@ -1350,9 +1302,10 @@ class AlexaModeController(AlexaCapability):
|
||||
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
AlexaCapability.__init__(self, entity, instance, non_controllable)
|
||||
super().__init__(entity, instance)
|
||||
self._resource = None
|
||||
self._semantics = None
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@@ -1455,8 +1408,8 @@ class AlexaModeController(AlexaCapability):
|
||||
modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, [])
|
||||
for mode in modes:
|
||||
self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode])
|
||||
# Humidifiers or Fans with a single mode completely break Alexa discovery,
|
||||
# add a fake preset (see issue #53832).
|
||||
# Humidifiers or Fans with a single mode completely break Alexa discovery, add a
|
||||
# fake preset (see issue #53832).
|
||||
if len(modes) == 1:
|
||||
self._resource.add_mode(
|
||||
f"{humidifier.ATTR_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||
@@ -1526,15 +1479,12 @@ class AlexaModeController(AlexaCapability):
|
||||
class AlexaRangeController(AlexaCapability):
|
||||
"""Implements Alexa.RangeController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController,
|
||||
ToggleController within the same device.
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property should be a concatenated string of device domain period
|
||||
and single word. e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property
|
||||
strings within the same device. e.g. Instance property cover.position &
|
||||
cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
@@ -1559,13 +1509,12 @@ class AlexaRangeController(AlexaCapability):
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, entity: State, instance: str | None, non_controllable: bool = False
|
||||
) -> None:
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
AlexaCapability.__init__(self, entity, instance, non_controllable)
|
||||
super().__init__(entity, instance)
|
||||
self._resource = None
|
||||
self._semantics = None
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@@ -1589,8 +1538,7 @@ class AlexaRangeController(AlexaCapability):
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
# Return None for unavailable and unknown states.
|
||||
# Allows the Alexa.EndpointHealth Interface to handle the unavailable
|
||||
# state in a stateReport.
|
||||
# Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport.
|
||||
if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
|
||||
return None
|
||||
|
||||
@@ -1619,10 +1567,6 @@ class AlexaRangeController(AlexaCapability):
|
||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
|
||||
# Number Value
|
||||
if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
|
||||
# Vacuum Fan Speed
|
||||
if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST)
|
||||
@@ -1700,29 +1644,7 @@ class AlexaRangeController(AlexaCapability):
|
||||
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
self._resource = AlexaPresetResource(
|
||||
["Value", get_resource_by_unit_of_measurement(self.entity)],
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
precision=precision,
|
||||
unit=unit,
|
||||
)
|
||||
self._resource.add_preset(
|
||||
value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM]
|
||||
)
|
||||
self._resource.add_preset(
|
||||
value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Number Value
|
||||
if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}":
|
||||
min_value = float(self.entity.attributes[number.ATTR_MIN])
|
||||
max_value = float(self.entity.attributes[number.ATTR_MAX])
|
||||
precision = float(self.entity.attributes.get(number.ATTR_STEP, 1))
|
||||
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
self._resource = AlexaPresetResource(
|
||||
["Value", get_resource_by_unit_of_measurement(self.entity)],
|
||||
["Value", AlexaGlobalCatalog.SETTING_PRESET],
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
precision=precision,
|
||||
@@ -1838,15 +1760,12 @@ class AlexaRangeController(AlexaCapability):
|
||||
class AlexaToggleController(AlexaCapability):
|
||||
"""Implements Alexa.ToggleController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController,
|
||||
ToggleController within the same device.
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property should be a concatenated string of device domain period
|
||||
and single word. e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property
|
||||
strings within the same device. e.g. Instance property cover.position
|
||||
& cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
@@ -1873,9 +1792,10 @@ class AlexaToggleController(AlexaCapability):
|
||||
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
AlexaCapability.__init__(self, entity, instance, non_controllable)
|
||||
super().__init__(entity, instance)
|
||||
self._resource = None
|
||||
self._semantics = None
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@@ -2101,8 +2021,7 @@ class AlexaEventDetectionSensor(AlexaCapability):
|
||||
state = self.entity.state
|
||||
|
||||
# Return None for unavailable and unknown states.
|
||||
# Allows the Alexa.EndpointHealth Interface to handle the unavailable
|
||||
# state in a stateReport.
|
||||
# Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport.
|
||||
if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
|
||||
return None
|
||||
|
||||
@@ -2170,8 +2089,7 @@ class AlexaEqualizerController(AlexaCapability):
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports.
|
||||
|
||||
Either bands, mode or both can be specified. Only mode is supported
|
||||
at this time.
|
||||
Either bands, mode or both can be specified. Only mode is supported at this time.
|
||||
"""
|
||||
return [{"name": "mode"}]
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Config helpers for Alexa."""
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -17,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AbstractConfig(ABC):
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
_unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None
|
||||
_unsub_proactive_report = None
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize abstract config."""
|
||||
|
||||
@@ -23,7 +23,6 @@ from homeassistant.components import (
|
||||
light,
|
||||
lock,
|
||||
media_player,
|
||||
number,
|
||||
scene,
|
||||
script,
|
||||
sensor,
|
||||
@@ -104,8 +103,7 @@ class DisplayCategory:
|
||||
# Indicates a device that cools the air in interior spaces.
|
||||
AIR_CONDITIONER = "AIR_CONDITIONER"
|
||||
|
||||
# Indicates a device that emits pleasant odors and masks unpleasant
|
||||
# odors in interior spaces.
|
||||
# Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces.
|
||||
AIR_FRESHENER = "AIR_FRESHENER"
|
||||
|
||||
# Indicates a device that improves the quality of air in interior spaces.
|
||||
@@ -145,8 +143,7 @@ class DisplayCategory:
|
||||
GAME_CONSOLE = "GAME_CONSOLE"
|
||||
|
||||
# Indicates a garage door.
|
||||
# Garage doors must implement the ModeController interface to
|
||||
# open and close the door.
|
||||
# Garage doors must implement the ModeController interface to open and close the door.
|
||||
GARAGE_DOOR = "GARAGE_DOOR"
|
||||
|
||||
# Indicates a wearable device that transmits audio directly into the ear.
|
||||
@@ -209,8 +206,8 @@ class DisplayCategory:
|
||||
# Indicates a security system.
|
||||
SECURITY_SYSTEM = "SECURITY_SYSTEM"
|
||||
|
||||
# Indicates an electric cooking device that sits on a countertop,
|
||||
# cooks at low temperatures, and is often shaped like a cooking pot.
|
||||
# Indicates an electric cooking device that sits on a countertop, cooks at low temperatures,
|
||||
# and is often shaped like a cooking pot.
|
||||
SLOW_COOKER = "SLOW_COOKER"
|
||||
|
||||
# Indicates an endpoint that locks.
|
||||
@@ -246,8 +243,7 @@ class DisplayCategory:
|
||||
# Indicates a vacuum cleaner.
|
||||
VACUUM_CLEANER = "VACUUM_CLEANER"
|
||||
|
||||
# Indicates a network-connected wearable device, such as an Apple Watch,
|
||||
# Fitbit, or Samsung Gear.
|
||||
# Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear.
|
||||
WEARABLE = "WEARABLE"
|
||||
|
||||
|
||||
@@ -578,10 +574,9 @@ class FanCapabilities(AlexaEntity):
|
||||
force_range_controller = False
|
||||
|
||||
# AlexaRangeController controls the Fan Speed Percentage.
|
||||
# For fans which only support on/off, no controller is added. This makes
|
||||
# the fan impossible to turn on or off through Alexa, most likely due
|
||||
# to a bug in Alexa. As a workaround, we add a range controller which
|
||||
# can only be set to 0% or 100%.
|
||||
# For fans which only support on/off, no controller is added. This makes the
|
||||
# fan impossible to turn on or off through Alexa, most likely due to a bug in Alexa.
|
||||
# As a workaround, we add a range controller which can only be set to 0% or 100%.
|
||||
if force_range_controller or supported & fan.FanEntityFeature.SET_SPEED:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}"
|
||||
@@ -854,9 +849,8 @@ class ImageProcessingCapabilities(AlexaEntity):
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(input_number.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(number.DOMAIN)
|
||||
class InputNumberCapabilities(AlexaEntity):
|
||||
"""Class to represent number and input_number capabilities."""
|
||||
"""Class to represent input_number capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
@@ -864,8 +858,10 @@ class InputNumberCapabilities(AlexaEntity):
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
domain = self.entity.domain
|
||||
yield AlexaRangeController(self.entity, instance=f"{domain}.value")
|
||||
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}"
|
||||
)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components import (
|
||||
input_number,
|
||||
light,
|
||||
media_player,
|
||||
number,
|
||||
timer,
|
||||
vacuum,
|
||||
)
|
||||
@@ -614,10 +613,9 @@ async def async_api_adjust_volume_step(
|
||||
"""Process an adjust volume step request."""
|
||||
# media_player volume up/down service does not support specifying steps
|
||||
# each component handles it differently e.g. via config.
|
||||
# This workaround will simply call the volume up/Volume down the amount of
|
||||
# steps asked for. When no steps are called in the request, Alexa sends
|
||||
# a default of 10 steps which for most purposes is too high. The default
|
||||
# is set 1 in this case.
|
||||
# This workaround will simply call the volume up/Volume down the amount of steps asked for
|
||||
# When no steps are called in the request, Alexa sends a default of 10 steps which for most
|
||||
# purposes is too high. The default is set 1 in this case.
|
||||
entity = directive.entity
|
||||
volume_int = int(directive.payload["volumeSteps"])
|
||||
is_default = bool(directive.payload["volumeStepsDefault"])
|
||||
@@ -1022,9 +1020,8 @@ async def async_api_disarm(
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
response = directive.response()
|
||||
|
||||
# Per Alexa Documentation: If you receive a Disarm directive, and the
|
||||
# system is already disarmed, respond with a success response,
|
||||
# not an error response.
|
||||
# Per Alexa Documentation: If you receive a Disarm directive, and the system is already disarmed,
|
||||
# respond with a success response, not an error response.
|
||||
if entity.state == STATE_ALARM_DISARMED:
|
||||
return response
|
||||
|
||||
@@ -1139,8 +1136,7 @@ async def async_api_adjust_mode(
|
||||
Only supportedModes with ordered=True support the adjustMode directive.
|
||||
"""
|
||||
|
||||
# Currently no supportedModes are configured with ordered=True
|
||||
# to support this request.
|
||||
# Currently no supportedModes are configured with ordered=True to support this request.
|
||||
raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED)
|
||||
|
||||
|
||||
@@ -1286,14 +1282,6 @@ async def async_api_set_range(
|
||||
max_value = float(entity.attributes[input_number.ATTR_MAX])
|
||||
data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value))
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{number.DOMAIN}.{number.ATTR_VALUE}":
|
||||
range_value = float(range_value)
|
||||
service = number.SERVICE_SET_VALUE
|
||||
min_value = float(entity.attributes[number.ATTR_MIN])
|
||||
max_value = float(entity.attributes[number.ATTR_MAX])
|
||||
data[number.ATTR_VALUE] = min(max_value, max(min_value, range_value))
|
||||
|
||||
# Vacuum Fan Speed
|
||||
elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
service = vacuum.SERVICE_SET_FAN_SPEED
|
||||
@@ -1425,17 +1413,6 @@ async def async_api_adjust_range(
|
||||
max_value, max(min_value, range_delta + current)
|
||||
)
|
||||
|
||||
# Number Value
|
||||
elif instance == f"{number.DOMAIN}.{number.ATTR_VALUE}":
|
||||
range_delta = float(range_delta)
|
||||
service = number.SERVICE_SET_VALUE
|
||||
min_value = float(entity.attributes[number.ATTR_MIN])
|
||||
max_value = float(entity.attributes[number.ATTR_MAX])
|
||||
current = float(entity.state)
|
||||
data[number.ATTR_VALUE] = response_value = min(
|
||||
max_value, max(min_value, range_delta + current)
|
||||
)
|
||||
|
||||
# Vacuum Fan Speed
|
||||
elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}":
|
||||
range_delta = int(range_delta)
|
||||
@@ -1506,9 +1483,7 @@ async def async_api_changechannel(
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
|
||||
media_player.const.MEDIA_TYPE_CHANNEL
|
||||
),
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
|
||||
@@ -193,6 +193,7 @@ def resolve_slot_synonyms(key, request):
|
||||
and "resolutionsPerAuthority" in request["resolutions"]
|
||||
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
|
||||
):
|
||||
|
||||
# Extract all of the possible values from each authority with a
|
||||
# successful match
|
||||
possible_values = []
|
||||
|
||||
@@ -6,15 +6,12 @@ class AlexaGlobalCatalog:
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog
|
||||
|
||||
You can use the global Alexa catalog for pre-defined names of devices, settings,
|
||||
values, and units.
|
||||
|
||||
You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units.
|
||||
This catalog is localized into all the languages that Alexa supports.
|
||||
You can reference the following catalog of pre-defined friendly names.
|
||||
|
||||
Each item in the following list is an asset identifier followed by its
|
||||
supported friendly names. The first friendly name for each identifier is
|
||||
the one displayed in the Alexa mobile app.
|
||||
You can reference the following catalog of pre-defined friendly names.
|
||||
Each item in the following list is an asset identifier followed by its supported friendly names.
|
||||
The first friendly name for each identifier is the one displayed in the Alexa mobile app.
|
||||
"""
|
||||
|
||||
# Air Purifier, Air Cleaner,Clean Air Machine
|
||||
@@ -26,8 +23,7 @@ class AlexaGlobalCatalog:
|
||||
# Router, Internet Router, Network Router, Wifi Router, Net Router
|
||||
DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router"
|
||||
|
||||
# Shade, Blind, Curtain, Roller, Shutter, Drape, Awning,
|
||||
# Window shade, Interior blind
|
||||
# Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind
|
||||
DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade"
|
||||
|
||||
# Shower
|
||||
@@ -194,13 +190,10 @@ class AlexaGlobalCatalog:
|
||||
|
||||
|
||||
class AlexaCapabilityResource:
|
||||
"""Base class for Alexa capabilityResources, modeResources, and presetResources.
|
||||
|
||||
Resources objects labels must be unique across all modeResources and
|
||||
presetResources within the same device. To provide support for all
|
||||
supported locales, include one label from the AlexaGlobalCatalog in the
|
||||
labels array.
|
||||
"""Base class for Alexa capabilityResources, modeResources, and presetResources objects.
|
||||
|
||||
Resources objects labels must be unique across all modeResources and presetResources within the same device.
|
||||
To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array.
|
||||
You cannot use any words from the following list as friendly names:
|
||||
https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use
|
||||
|
||||
@@ -218,17 +211,11 @@ class AlexaCapabilityResource:
|
||||
return self.serialize_labels(self._resource_labels)
|
||||
|
||||
def serialize_configuration(self):
|
||||
"""Return serialized configuration for an API response.
|
||||
|
||||
Return ModeResources, PresetResources friendlyNames serialized.
|
||||
"""
|
||||
"""Return ModeResources, PresetResources friendlyNames serialized for an API response."""
|
||||
return []
|
||||
|
||||
def serialize_labels(self, resources):
|
||||
"""Return serialized labels for an API response.
|
||||
|
||||
Returns resource label objects for friendlyNames serialized.
|
||||
"""
|
||||
"""Return resource label objects for friendlyNames serialized for an API response."""
|
||||
labels = []
|
||||
for label in resources:
|
||||
if label in AlexaGlobalCatalog.__dict__.values():
|
||||
@@ -258,10 +245,7 @@ class AlexaModeResource(AlexaCapabilityResource):
|
||||
self._supported_modes.append({"value": value, "labels": labels})
|
||||
|
||||
def serialize_configuration(self):
|
||||
"""Return serialized configuration for an API response.
|
||||
|
||||
Returns configuration for ModeResources friendlyNames serialized.
|
||||
"""
|
||||
"""Return configuration for ModeResources friendlyNames serialized for an API response."""
|
||||
mode_resources = []
|
||||
for mode in self._supported_modes:
|
||||
result = {
|
||||
@@ -276,8 +260,7 @@ class AlexaModeResource(AlexaCapabilityResource):
|
||||
class AlexaPresetResource(AlexaCapabilityResource):
|
||||
"""Implements Alexa PresetResources.
|
||||
|
||||
Use presetResources with RangeController to provide a set of
|
||||
friendlyNamesfor each RangeController preset.
|
||||
Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
|
||||
"""
|
||||
@@ -298,10 +281,7 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
||||
self._presets.append({"value": value, "labels": labels})
|
||||
|
||||
def serialize_configuration(self):
|
||||
"""Return serialized configuration for an API response.
|
||||
|
||||
Returns configuration for PresetResources friendlyNames serialized.
|
||||
"""
|
||||
"""Return configuration for PresetResources friendlyNames serialized for an API response."""
|
||||
configuration = {
|
||||
"supportedRange": {
|
||||
"minimumValue": self._minimum_value,
|
||||
@@ -329,23 +309,18 @@ class AlexaPresetResource(AlexaCapabilityResource):
|
||||
class AlexaSemantics:
|
||||
"""Class for Alexa Semantics Object.
|
||||
|
||||
You can optionally enable additional utterances by using semantics. When
|
||||
you use semantics, you manually map the phrases "open", "close", "raise",
|
||||
and "lower" to directives.
|
||||
You can optionally enable additional utterances by using semantics. When you use semantics,
|
||||
you manually map the phrases "open", "close", "raise", and "lower" to directives.
|
||||
|
||||
Semantics is supported for the following interfaces only: ModeController,
|
||||
RangeController, and ToggleController.
|
||||
Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController.
|
||||
|
||||
Semantics stateMappings are only supported for one interface of the same
|
||||
type on the same device. If a device has multiple RangeControllers only
|
||||
one interface may use stateMappings otherwise discovery will fail.
|
||||
Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has
|
||||
multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail.
|
||||
|
||||
You can support semantics actionMappings on different controllers for the
|
||||
same device, however each controller must support different phrases.
|
||||
For example, you can support "raise" on a RangeController, and "open"
|
||||
on a ModeController, but you can't support "open" on both RangeController
|
||||
and ModeController. Semantics stateMappings are only supported for one
|
||||
interface on the same device.
|
||||
You can support semantics actionMappings on different controllers for the same device, however each controller must
|
||||
support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController,
|
||||
but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported
|
||||
for one interface on the same device.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||
"""
|
||||
|
||||
325
homeassistant/components/almond/__init__.py
Normal file
325
homeassistant/components/almond/__init__.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""Support for Almond."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError, ClientSession
|
||||
import async_timeout
|
||||
from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_HOST,
|
||||
CONF_TYPE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
from homeassistant.core import Context, CoreState, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
event,
|
||||
intent,
|
||||
network,
|
||||
storage,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import config_flow
|
||||
from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = DOMAIN
|
||||
|
||||
ALMOND_SETUP_DELAY = 30
|
||||
|
||||
DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu"
|
||||
DEFAULT_LOCAL_HOST = "http://localhost:3000"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TYPE): TYPE_OAUTH2,
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_OAUTH2_HOST): cv.url,
|
||||
}
|
||||
),
|
||||
vol.Schema(
|
||||
{vol.Required(CONF_TYPE): TYPE_LOCAL, vol.Required(CONF_HOST): cv.url}
|
||||
),
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Almond component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
host = conf[CONF_HOST]
|
||||
|
||||
if conf[CONF_TYPE] == TYPE_OAUTH2:
|
||||
config_flow.AlmondFlowHandler.async_register_implementation(
|
||||
hass,
|
||||
config_entry_oauth2_flow.LocalOAuth2Implementation(
|
||||
hass,
|
||||
DOMAIN,
|
||||
conf[CONF_CLIENT_ID],
|
||||
conf[CONF_CLIENT_SECRET],
|
||||
f"{host}/me/api/oauth2/authorize",
|
||||
f"{host}/me/api/oauth2/token",
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
if not hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]},
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Almond config entry."""
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
if entry.data["type"] == TYPE_LOCAL:
|
||||
auth = AlmondLocalAuth(entry.data["host"], websession)
|
||||
else:
|
||||
# OAuth2
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
oauth_session = config_entry_oauth2_flow.OAuth2Session(
|
||||
hass, entry, implementation
|
||||
)
|
||||
auth = AlmondOAuth(entry.data["host"], websession, oauth_session)
|
||||
|
||||
api = WebAlmondAPI(auth)
|
||||
agent = AlmondAgent(hass, api, entry)
|
||||
|
||||
# Hass.io does its own configuration.
|
||||
if not entry.data.get("is_hassio"):
|
||||
# If we're not starting or local, set up Almond right away
|
||||
if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL:
|
||||
await _configure_almond_for_ha(hass, entry, api)
|
||||
|
||||
else:
|
||||
# OAuth2 implementations can potentially rely on the HA Cloud url.
|
||||
# This url is not be available until 30 seconds after boot.
|
||||
|
||||
async def configure_almond(_now):
|
||||
try:
|
||||
await _configure_almond_for_ha(hass, entry, api)
|
||||
except ConfigEntryNotReady:
|
||||
_LOGGER.warning(
|
||||
"Unable to configure Almond to connect to Home Assistant"
|
||||
)
|
||||
|
||||
async def almond_hass_start(_event):
|
||||
event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start)
|
||||
|
||||
conversation.async_set_agent(hass, agent)
|
||||
return True
|
||||
|
||||
|
||||
async def _configure_almond_for_ha(
|
||||
hass: HomeAssistant, entry: ConfigEntry, api: WebAlmondAPI
|
||||
):
|
||||
"""Configure Almond to connect to HA."""
|
||||
try:
|
||||
if entry.data["type"] == TYPE_OAUTH2:
|
||||
# If we're connecting over OAuth2, we will only set up connection
|
||||
# with Home Assistant if we're remotely accessible.
|
||||
hass_url = network.get_url(hass, allow_internal=False, prefer_cloud=True)
|
||||
else:
|
||||
hass_url = network.get_url(hass)
|
||||
except network.NoURLAvailableError:
|
||||
# If no URL is available, we're not going to configure Almond to connect to HA.
|
||||
return
|
||||
|
||||
_LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url)
|
||||
store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY)
|
||||
data = await store.async_load()
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
user = None
|
||||
if "almond_user" in data:
|
||||
user = await hass.auth.async_get_user(data["almond_user"])
|
||||
|
||||
if user is None:
|
||||
user = await hass.auth.async_create_system_user(
|
||||
"Almond", group_ids=[GROUP_ID_ADMIN]
|
||||
)
|
||||
data["almond_user"] = user.id
|
||||
await store.async_save(data)
|
||||
|
||||
refresh_token = await hass.auth.async_create_refresh_token(
|
||||
user,
|
||||
# Almond will be fine as long as we restart once every 5 years
|
||||
access_token_expiration=timedelta(days=365 * 5),
|
||||
)
|
||||
|
||||
# Create long lived access token
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
# Store token in Almond
|
||||
try:
|
||||
async with async_timeout.timeout(30):
|
||||
await api.async_create_device(
|
||||
{
|
||||
"kind": "io.home-assistant",
|
||||
"hassUrl": hass_url,
|
||||
"accessToken": access_token,
|
||||
"refreshToken": "",
|
||||
# 5 years from now in ms.
|
||||
"accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000,
|
||||
}
|
||||
)
|
||||
except (asyncio.TimeoutError, ClientError) as err:
|
||||
if isinstance(err, asyncio.TimeoutError):
|
||||
msg: str | ClientError = "Request timeout"
|
||||
else:
|
||||
msg = err
|
||||
_LOGGER.warning("Unable to configure Almond: %s", msg)
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
# Clear all other refresh tokens
|
||||
for token in list(user.refresh_tokens.values()):
|
||||
if token.id != refresh_token.id:
|
||||
await hass.auth.async_remove_refresh_token(token)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Almond."""
|
||||
conversation.async_set_agent(hass, None)
|
||||
return True
|
||||
|
||||
|
||||
class AlmondOAuth(AbstractAlmondWebAuth):
|
||||
"""Almond Authentication using OAuth2."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
websession: ClientSession,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize Almond auth."""
|
||||
super().__init__(host, websession)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Return a valid access token."""
|
||||
if not self._oauth_session.valid_token:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return self._oauth_session.token["access_token"]
|
||||
|
||||
|
||||
class AlmondAgent(conversation.AbstractConversationAgent):
|
||||
"""Almond conversation agent."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: WebAlmondAPI, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Initialize the agent."""
|
||||
self.hass = hass
|
||||
self.api = api
|
||||
self.entry = entry
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"}
|
||||
|
||||
async def async_get_onboarding(self):
|
||||
"""Get onboard url if not onboarded."""
|
||||
if self.entry.data.get("onboarded"):
|
||||
return None
|
||||
|
||||
host = self.entry.data["host"]
|
||||
if self.entry.data.get("is_hassio"):
|
||||
host = "/core_almond"
|
||||
return {
|
||||
"text": (
|
||||
"Would you like to opt-in to share your anonymized commands with"
|
||||
" Stanford to improve Almond's responses?"
|
||||
),
|
||||
"url": f"{host}/conversation",
|
||||
}
|
||||
|
||||
async def async_set_onboarding(self, shown):
|
||||
"""Set onboarding status."""
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry, data={**self.entry.data, "onboarded": shown}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def async_process(
|
||||
self,
|
||||
text: str,
|
||||
context: Context,
|
||||
conversation_id: str | None = None,
|
||||
language: str | None = None,
|
||||
) -> conversation.ConversationResult | None:
|
||||
"""Process a sentence."""
|
||||
response = await self.api.async_converse_text(text, conversation_id)
|
||||
language = language or self.hass.config.language
|
||||
|
||||
first_choice = True
|
||||
buffer = ""
|
||||
for message in response["messages"]:
|
||||
if message["type"] == "text":
|
||||
buffer += f"\n{message['text']}"
|
||||
elif message["type"] == "picture":
|
||||
buffer += f"\n Picture: {message['url']}"
|
||||
elif message["type"] == "rdl":
|
||||
buffer += (
|
||||
f"\n Link: {message['rdl']['displayTitle']} "
|
||||
f"{message['rdl']['webCallback']}"
|
||||
)
|
||||
elif message["type"] == "choice":
|
||||
if first_choice:
|
||||
first_choice = False
|
||||
else:
|
||||
buffer += ","
|
||||
buffer += f" {message['title']}"
|
||||
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_speech(buffer.strip())
|
||||
return conversation.ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
)
|
||||
124
homeassistant/components/almond/config_flow.py
Normal file
124
homeassistant/components/almond/config_flow.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Config flow to connect with Home Assistant."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
import async_timeout
|
||||
from pyalmond import AlmondLocalAuth, WebAlmondAPI
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant import core, data_entry_flow
|
||||
from homeassistant.components.hassio import HassioServiceInfo
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2
|
||||
|
||||
|
||||
async def async_verify_local_connection(hass: core.HomeAssistant, host: str):
|
||||
"""Verify that a local connection works."""
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
api = WebAlmondAPI(AlmondLocalAuth(host, websession))
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
await api.async_list_apps()
|
||||
|
||||
return True
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
return False
|
||||
|
||||
|
||||
class AlmondFlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
):
|
||||
"""Implementation of the Almond OAuth2 config flow."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
host = None
|
||||
hassio_discovery = None
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {"scope": "profile user-read user-read-results user-exec-command"}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
# Only allow 1 instance.
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return await super().async_step_user(user_input)
|
||||
|
||||
async def async_step_auth(self, user_input=None):
|
||||
"""Handle authorize step."""
|
||||
result = await super().async_step_auth(user_input)
|
||||
|
||||
if result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP:
|
||||
self.host = str(URL(result["url"]).with_path("me"))
|
||||
|
||||
return result
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> FlowResult:
|
||||
"""Create an entry for the flow.
|
||||
|
||||
Ok to override if you want to fetch extra info or even add another step.
|
||||
"""
|
||||
data["type"] = TYPE_OAUTH2
|
||||
data["host"] = self.host
|
||||
return self.async_create_entry(title=self.flow_impl.name, data=data)
|
||||
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
|
||||
"""Import data."""
|
||||
# Only allow 1 instance.
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
if not await async_verify_local_connection(self.hass, user_input["host"]):
|
||||
self.logger.warning(
|
||||
"Aborting import of Almond because we're unable to connect"
|
||||
)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Configuration.yaml",
|
||||
data={"type": TYPE_LOCAL, "host": user_input["host"]},
|
||||
)
|
||||
|
||||
async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult:
|
||||
"""Receive a Hass.io discovery."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
self.hassio_discovery = discovery_info.config
|
||||
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
async def async_step_hassio_confirm(self, user_input=None):
|
||||
"""Confirm a Hass.io discovery."""
|
||||
data = self.hassio_discovery
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=data["addon"],
|
||||
data={
|
||||
"is_hassio": True,
|
||||
"type": TYPE_LOCAL,
|
||||
"host": f"http://{data['host']}:{data['port']}",
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="hassio_confirm",
|
||||
description_placeholders={"addon": data["addon"]},
|
||||
)
|
||||
4
homeassistant/components/almond/const.py
Normal file
4
homeassistant/components/almond/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the Almond integration."""
|
||||
DOMAIN = "almond"
|
||||
TYPE_OAUTH2 = "oauth2"
|
||||
TYPE_LOCAL = "local"
|
||||
11
homeassistant/components/almond/manifest.json
Normal file
11
homeassistant/components/almond/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "almond",
|
||||
"name": "Almond",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/almond",
|
||||
"dependencies": ["auth", "conversation"],
|
||||
"codeowners": ["@gcampax", "@balloob"],
|
||||
"requirements": ["pyalmond==0.0.2"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyalmond"]
|
||||
}
|
||||
19
homeassistant/components/almond/strings.json
Normal file
19
homeassistant/components/almond/strings.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "Almond via Home Assistant add-on",
|
||||
"description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
homeassistant/components/almond/translations/bg.json
Normal file
14
homeassistant/components/almond/translations/bg.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
|
||||
"missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond.",
|
||||
"single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f."
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
homeassistant/components/almond/translations/ca.json
Normal file
19
homeassistant/components/almond/translations/ca.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||
"missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
|
||||
"no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
|
||||
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?",
|
||||
"title": "Almond via complement de Home Assistant"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "Selecciona el m\u00e8tode d'autenticaci\u00f3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
homeassistant/components/almond/translations/cs.json
Normal file
19
homeassistant/components/almond/translations/cs.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
|
||||
"missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.",
|
||||
"no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})",
|
||||
"single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed Supervisor {addon}?",
|
||||
"title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Supervisor"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "Vyberte metodu ov\u011b\u0159en\u00ed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
homeassistant/components/almond/translations/da.json
Normal file
17
homeassistant/components/almond/translations/da.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.",
|
||||
"missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Supervisor-tilf\u00f8jelsen: {addon}?",
|
||||
"title": "Almond via Supervisor-tilf\u00f8jelse"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "V\u00e6lg godkendelsesmetode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
homeassistant/components/almond/translations/de.json
Normal file
19
homeassistant/components/almond/translations/de.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
|
||||
"no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
|
||||
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Supervisor-Add-On hergestellt wird: {addon}?",
|
||||
"title": "Almond \u00fcber das Supervisor Add-on"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "W\u00e4hle die Authentifizierungsmethode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user