Compare commits

..

3 Commits

Author SHA1 Message Date
Erik
18dae08244 Set unique_id to DOMAIN 2023-01-17 19:03:17 +01:00
Erik
5d21b6e7a7 Validate URL 2023-01-17 16:33:19 +01:00
Erik
eea98b22e0 Allow manually setting up the Thread integration 2023-01-17 16:01:02 +01:00
2562 changed files with 12294 additions and 41764 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -10,7 +10,7 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.10"
DEFAULT_PYTHON: 3.9
jobs:
init:

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ on:
- "**strings.json"
env:
DEFAULT_PYTHON: "3.10"
DEFAULT_PYTHON: 3.9
jobs:
upload:

View File

@@ -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$

View File

@@ -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
View File

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

View File

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

View File

@@ -5,8 +5,6 @@ FROM ${BUILD_FROM}
ENV \
S6_SERVICES_GRACETIME=220000
ARG QEMU_CPU
WORKDIR /usr/src
## Setup Home Assistant Core dependencies

View File

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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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(

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

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

View File

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

View File

@@ -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:

View File

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

View File

@@ -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,

View File

@@ -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)
)

View File

@@ -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
)

View File

@@ -6,4 +6,5 @@ LOGGER = logging.getLogger(__package__)
DOMAIN = "abode"
ATTRIBUTION = "Data provided by goabode.com"
DEFAULT_CACHEDB = "abodepy_cache.pickle"
CONF_POLLING = "polling"

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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]

View File

@@ -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),

View File

@@ -1,8 +1,5 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
},
"step": {
"cloud": {
"data": {

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -1,12 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"ip_address": "IP-adress",
"password": "L\u00f6senord"
}
}
}
}
}

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -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},
)
)

View File

@@ -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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,13 @@
"config": {
"error": {
"general_error": "Tapahtui tuntematon virhe."
},
"step": {
"node_pro": {
"data": {
"password": "Salasana"
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
{
"config": {
"step": {
"user": {
"node_pro": {
"data": {
"name": "\u041d\u0430\u0437\u0432\u0430"
"password": "Slapta\u017eodis"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,12 @@
"latitude": "Latitude"
}
},
"node_pro": {
"data": {
"ip_address": "Endere\u00e7o",
"password": "Palavra-passe"
}
},
"reauth_confirm": {
"data": {
"api_key": "Chave da API"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -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())

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

@@ -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={

View File

@@ -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"],

View File

@@ -1,7 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Ier\u012bce jau pievienota Home Assistant."
}
}
}

View File

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

View File

@@ -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):

View File

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

View File

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

View File

@@ -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)

View File

@@ -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(

View File

@@ -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 = []

View File

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

View 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
)

View 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"]},
)

View File

@@ -0,0 +1,4 @@
"""Constants for the Almond integration."""
DOMAIN = "almond"
TYPE_OAUTH2 = "oauth2"
TYPE_LOCAL = "local"

View 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"]
}

View 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%]"
}
}
}

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

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

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

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

View 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