Merge pull request #28909 from home-assistant/rc

0.102.0
This commit is contained in:
Paulus Schoutsen 2019-11-20 14:04:30 -08:00 committed by GitHub
commit df3e17a983
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
820 changed files with 22874 additions and 5160 deletions

View File

@ -69,6 +69,7 @@ omit =
homeassistant/components/avea/light.py homeassistant/components/avea/light.py
homeassistant/components/avion/light.py homeassistant/components/avion/light.py
homeassistant/components/azure_event_hub/* homeassistant/components/azure_event_hub/*
homeassistant/components/azure_service_bus/*
homeassistant/components/baidu/tts.py homeassistant/components/baidu/tts.py
homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/beewi_smartclim/sensor.py
homeassistant/components/bbb_gpio/* homeassistant/components/bbb_gpio/*
@ -274,7 +275,6 @@ omit =
homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor.py
homeassistant/components/gstreamer/media_player.py homeassistant/components/gstreamer/media_player.py
homeassistant/components/gtfs/sensor.py homeassistant/components/gtfs/sensor.py
homeassistant/components/gtt/sensor.py
homeassistant/components/habitica/* homeassistant/components/habitica/*
homeassistant/components/hangouts/* homeassistant/components/hangouts/*
homeassistant/components/hangouts/__init__.py homeassistant/components/hangouts/__init__.py
@ -499,6 +499,7 @@ omit =
homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_bluray/media_player.py
homeassistant/components/panasonic_viera/media_player.py homeassistant/components/panasonic_viera/media_player.py
homeassistant/components/pandora/media_player.py homeassistant/components/pandora/media_player.py
homeassistant/components/pcal9535a/*
homeassistant/components/pencom/switch.py homeassistant/components/pencom/switch.py
homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/media_player.py
homeassistant/components/pi_hole/sensor.py homeassistant/components/pi_hole/sensor.py
@ -720,6 +721,7 @@ omit =
homeassistant/components/uber/sensor.py homeassistant/components/uber/sensor.py
homeassistant/components/ubus/device_tracker.py homeassistant/components/ubus/device_tracker.py
homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ue_smart_radio/media_player.py
homeassistant/components/unifiled/*
homeassistant/components/upcloud/* homeassistant/components/upcloud/*
homeassistant/components/upnp/* homeassistant/components/upnp/*
homeassistant/components/upc_connect/* homeassistant/components/upc_connect/*

View File

@ -0,0 +1,42 @@
# This configuration includes the full set of hooks we use. In
# addition to the defaults (see .pre-commit-config.yaml), this
# includes hooks that require our development and test dependencies
# installed and the virtualenv containing them active by the time
# pre-commit runs to produce correct results.
#
# If this is not a problem for your workflow, using this config is
# recommended, install it with
# pre-commit install --config .pre-commit-config-all.yaml
# Otherwise, see the default .pre-commit-config.yaml for a lighter one.
repos:
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
args:
- --safe
- --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==4.0.1
files: ^(homeassistant|script|tests)/.+\.py$
# Using a local "system" mypy instead of the mypy hook, because its
# results depend on what is installed. And the mypy hook runs in a
# virtualenv of its own, meaning we'd need to install and maintain
# another set of our dependencies there... no. Use the "system" one
# and reuse the environment that is set up anyway already instead.
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
types: [python]
require_serial: true
files: ^homeassistant/.+\.py$

View File

@ -1,6 +1,13 @@
# This configuration includes the default, minimal set of hooks to be
# run on all commits. It requires no specific setup and one can just
# start using pre-commit with it.
#
# See .pre-commit-config-all.yaml for a more complete one that comes
# with a better coverage at the cost of some specific setup needed.
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 19.3b0 rev: 19.10b0
hooks: hooks:
- id: black - id: black
args: args:
@ -8,24 +15,10 @@ repos:
- --quiet - --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 3.7.8 rev: 3.7.9
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
- flake8-docstrings==1.3.1 - flake8-docstrings==1.5.0
- pydocstyle==4.0.0 - pydocstyle==4.0.1
files: ^(homeassistant|script|tests)/.+\.py$ files: ^(homeassistant|script|tests)/.+\.py$
# Using a local "system" mypy instead of the mypy hook, because its
# results depend on what is installed. And the mypy hook runs in a
# virtualenv of its own, meaning we'd need to install and maintain
# another set of our dependencies there... no. Use the "system" one
# and reuse the environment that is set up anyway already instead.
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
language: system
types: [python]
require_serial: true
files: ^homeassistant/.+\.py$

View File

@ -19,7 +19,7 @@ matrix:
- python: "3.6.1" - python: "3.6.1"
env: TOXENV=lint env: TOXENV=lint
- python: "3.6.1" - python: "3.6.1"
env: TOXENV=pylint PYLINT_ARGS=--jobs=0 env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30
- python: "3.6.1" - python: "3.6.1"
env: TOXENV=typing env: TOXENV=typing
- python: "3.6.1" - python: "3.6.1"
@ -33,4 +33,4 @@ cache:
- $HOME/.cache/pre-commit - $HOME/.cache/pre-commit
install: pip install -U tox install: pip install -U tox
language: python language: python
script: travis_wait 50 tox --develop script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop

2
.vscode/tasks.json vendored
View File

@ -33,7 +33,7 @@
{ {
"label": "Flake8", "label": "Flake8",
"type": "shell", "type": "shell",
"command": "flake8 homeassistant tests", "command": "pre-commit run flake8 --all-files",
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true

View File

@ -19,6 +19,7 @@ homeassistant/components/airly/* @bieniu
homeassistant/components/airvisual/* @bachya homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
homeassistant/components/almond/* @gcampax @balloob
homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/amazon_polly/* @robbiet480
homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambiclimate/* @danielhiversen
@ -42,6 +43,7 @@ homeassistant/components/awair/* @danielsjf
homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610 homeassistant/components/axis/* @kane610
homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_event_hub/* @eavanvalkenburg
homeassistant/components/azure_service_bus/* @hfurubotten
homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/beewi_smartclim/* @alemuro
homeassistant/components/bitcoin/* @fabaff homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/bizkaibus/* @UgaitzEtxebarria
@ -59,6 +61,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl
homeassistant/components/ciscospark/* @fbradyirl homeassistant/components/ciscospark/* @fbradyirl
homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloud/* @home-assistant/cloud
homeassistant/components/cloudflare/* @ludeeus homeassistant/components/cloudflare/* @ludeeus
homeassistant/components/comfoconnect/* @michaelarnauts
homeassistant/components/config/* @home-assistant/core homeassistant/components/config/* @home-assistant/core
homeassistant/components/configurator/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core
homeassistant/components/conversation/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core
@ -155,9 +158,11 @@ homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/izone/* @Swamp-Ig homeassistant/components/izone/* @Swamp-Ig
homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/juicenet/* @jesserockz
homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/kaiterra/* @Michsior14
homeassistant/components/keba/* @dannerph homeassistant/components/keba/* @dannerph
homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/keyboard_remote/* @bendavid
homeassistant/components/knx/* @Julius2342 homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills homeassistant/components/kodi/* @armills
homeassistant/components/konnected/* @heythisisnate homeassistant/components/konnected/* @heythisisnate
@ -173,6 +178,7 @@ homeassistant/components/logi_circle/* @evanjd
homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luci/* @fbradyirl @mzdrale
homeassistant/components/luftdaten/* @fabaff homeassistant/components/luftdaten/* @fabaff
homeassistant/components/lutron/* @JonGilmore
homeassistant/components/mastodon/* @fabaff homeassistant/components/mastodon/* @fabaff
homeassistant/components/matrix/* @tinloaf homeassistant/components/matrix/* @tinloaf
homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mcp23017/* @jardiamj
@ -221,13 +227,14 @@ homeassistant/components/oru/* @bvlaicu
homeassistant/components/owlet/* @oblogic7 homeassistant/components/owlet/* @oblogic7
homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend
homeassistant/components/pcal9535a/* @Shulyaka
homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus homeassistant/components/philips_js/* @elupus
homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/pi_hole/* @fabaff @johnluetke
homeassistant/components/plaato/* @JohNan homeassistant/components/plaato/* @JohNan
homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plant/* @ChristianKuehnel
homeassistant/components/plex/* @jjlawren homeassistant/components/plex/* @jjlawren
homeassistant/components/plugwise/* @laetificat @CoMPaTech homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew
homeassistant/components/point/* @fredrike homeassistant/components/point/* @fredrike
homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ps4/* @ktnrg45
homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/ptvsd/* @swamp-ig
@ -247,6 +254,7 @@ homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt homeassistant/components/roomba/* @pschmitt
homeassistant/components/saj/* @fredericvl homeassistant/components/saj/* @fredericvl
homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core homeassistant/components/scene/* @home-assistant/core
homeassistant/components/scrape/* @fabaff homeassistant/components/scrape/* @fabaff
homeassistant/components/script/* @home-assistant/core homeassistant/components/script/* @home-assistant/core
@ -277,6 +285,7 @@ homeassistant/components/sql/* @dgomes
homeassistant/components/statistics/* @fabaff homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stiebel_eltron/* @fucm
homeassistant/components/stream/* @hunterjm homeassistant/components/stream/* @hunterjm
homeassistant/components/stt/* @pvizeli
homeassistant/components/suez_water/* @ooii homeassistant/components/suez_water/* @ooii
homeassistant/components/sun/* @Swamp-Ig homeassistant/components/sun/* @Swamp-Ig
homeassistant/components/supla/* @mwegrzynek homeassistant/components/supla/* @mwegrzynek
@ -305,12 +314,13 @@ homeassistant/components/tplink/* @rytilahti
homeassistant/components/traccar/* @ludeeus homeassistant/components/traccar/* @ludeeus
homeassistant/components/tradfri/* @ggravlingen homeassistant/components/tradfri/* @ggravlingen
homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_train/* @endor-force
homeassistant/components/transmission/* @engrbm87 homeassistant/components/transmission/* @engrbm87 @JPHutchins
homeassistant/components/tts/* @robbiet480 homeassistant/components/tts/* @robbiet480
homeassistant/components/twentemilieu/* @frenck homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_call/* @robbiet480
homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480
homeassistant/components/unifi/* @kane610 homeassistant/components/unifi/* @kane610
homeassistant/components/unifiled/* @florisvdk
homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upc_connect/* @pvizeli
homeassistant/components/upcloud/* @scop homeassistant/components/upcloud/* @scop
homeassistant/components/updater/* @home-assistant/core homeassistant/components/updater/* @home-assistant/core
@ -333,6 +343,7 @@ homeassistant/components/weblink/* @home-assistant/core
homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @sqldiablo homeassistant/components/wemo/* @sqldiablo
homeassistant/components/withings/* @vangorra homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck
homeassistant/components/worldclock/* @fabaff homeassistant/components/worldclock/* @fabaff
homeassistant/components/wwlln/* @bachya homeassistant/components/wwlln/* @bachya
homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xbox_live/* @MartinHjelmare

View File

@ -24,9 +24,9 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
WORKDIR /workspaces WORKDIR /workspaces
# Install Python dependencies from requirements # Install Python dependencies from requirements
COPY requirements_test.txt homeassistant/package_constraints.txt ./ COPY requirements_test.txt requirements_test_pre_commit.txt homeassistant/package_constraints.txt ./
RUN pip3 install -r requirements_test.txt -c package_constraints.txt \ RUN pip3 install -r requirements_test.txt -c package_constraints.txt \
&& rm -f requirements_test.txt package_constraints.txt && rm -f requirements_test.txt package_constraints.txt requirements_test_pre_commit.txt
# Set the default shell to bash instead of sh # Set the default shell to bash instead of sh
ENV SHELL /bin/bash ENV SHELL /bin/bash

View File

@ -45,7 +45,7 @@ stages:
. venv/bin/activate . venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks pre-commit install-hooks --config .pre-commit-config-all.yaml
- script: | - script: |
. venv/bin/activate . venv/bin/activate
pre-commit run flake8 --all-files pre-commit run flake8 --all-files
@ -84,7 +84,7 @@ stages:
. venv/bin/activate . venv/bin/activate
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks pre-commit install-hooks --config .pre-commit-config-all.yaml
- script: | - script: |
. venv/bin/activate . venv/bin/activate
pre-commit run black --all-files pre-commit run black --all-files
@ -127,7 +127,7 @@ stages:
set -e set -e
. venv/bin/activate . venv/bin/activate
pytest --timeout=9 --durations=10 -n 2 --dist loadfile -qq -o console_output_style=count -p no:sugar tests pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar tests
script/check_dirty script/check_dirty
displayName: 'Run pytest for python $(python.container)' displayName: 'Run pytest for python $(python.container)'
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
@ -135,7 +135,7 @@ stages:
set -e set -e
. venv/bin/activate . venv/bin/activate
pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
codecov --token $(codecovToken) codecov --token $(codecovToken)
script/check_dirty script/check_dirty
displayName: 'Run pytest for python $(python.container) / coverage' displayName: 'Run pytest for python $(python.container) / coverage'
@ -182,8 +182,8 @@ stages:
. venv/bin/activate . venv/bin/activate
pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt
pre-commit install-hooks pre-commit install-hooks --config .pre-commit-config-all.yaml
- script: | - script: |
. venv/bin/activate . venv/bin/activate
pre-commit run mypy --all-files pre-commit run --config .pre-commit-config-all.yaml mypy --all-files
displayName: 'Run mypy' displayName: 'Run mypy'

View File

@ -261,7 +261,7 @@ class AuthManager:
"""Enable a multi-factor auth module for user.""" """Enable a multi-factor auth module for user."""
if user.system_generated: if user.system_generated:
raise ValueError( raise ValueError(
"System generated users cannot enable " "multi-factor auth module." "System generated users cannot enable multi-factor auth module."
) )
module = self.get_auth_mfa_module(mfa_module_id) module = self.get_auth_mfa_module(mfa_module_id)
@ -276,7 +276,7 @@ class AuthManager:
"""Disable a multi-factor auth module for user.""" """Disable a multi-factor auth module for user."""
if user.system_generated: if user.system_generated:
raise ValueError( raise ValueError(
"System generated users cannot disable " "multi-factor auth module." "System generated users cannot disable multi-factor auth module."
) )
module = self.get_auth_mfa_module(mfa_module_id) module = self.get_auth_mfa_module(mfa_module_id)
@ -320,7 +320,7 @@ class AuthManager:
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError( raise ValueError(
"System generated users can only have system type " "refresh tokens" "System generated users can only have system type refresh tokens"
) )
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
@ -330,7 +330,7 @@ class AuthManager:
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
and client_name is None and client_name is None
): ):
raise ValueError("Client_name is required for long-lived access " "token") raise ValueError("Client_name is required for long-lived access token")
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values(): for token in user.refresh_tokens.values():

View File

@ -215,7 +215,11 @@ class TotpSetupFlow(SetupFlow):
else: else:
hass = self._auth_module.hass hass = self._auth_module.hass
self._ota_secret, self._url, self._image = await hass.async_add_executor_job( (
self._ota_secret,
self._url,
self._image,
) = await hass.async_add_executor_job(
_generate_secret_and_qr_code, # type: ignore _generate_secret_and_qr_code, # type: ignore
str(self._user.name), str(self._user.name),
) )

View File

@ -33,6 +33,8 @@ STAGE_1_INTEGRATIONS = {
"recorder", "recorder",
# To make sure we forward data to other instances # To make sure we forward data to other instances
"mqtt_eventstream", "mqtt_eventstream",
# To provide account link implementations
"cloud",
} }

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode."
},
"error": {
"connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Abode.",
"identifier_exists": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d.",
"invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438."
},
"step": {
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u0430",
"username": "E-mail \u0430\u0434\u0440\u0435\u0441"
},
"title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0412\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0432\u0445\u043e\u0434 \u0432 Abode"
}
},
"title": "Abode"
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"single_instance_allowed": "Je povolena pouze jedna konfigurace Abode."
},
"error": {
"connection_error": "Nelze se p\u0159ipojit k Abode.",
"identifier_exists": "\u00da\u010det je ji\u017e zaregistrov\u00e1n.",
"invalid_credentials": "Neplatn\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje."
},
"step": {
"user": {
"data": {
"password": "Heslo",
"username": "E-mailov\u00e1 adresa"
},
"title": "Vypl\u0148te p\u0159ihla\u0161ovac\u00ed \u00fadaje Abode"
}
},
"title": "Abode"
}
}

View File

@ -1,8 +1,17 @@
{ {
"config": { "config": {
"abort": {
"single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Abode \u00e9 permitida."
},
"error": {
"connection_error": "N\u00e3o foi poss\u00edvel conectar ao Abode.",
"identifier_exists": "Conta j\u00e1 cadastrada.",
"invalid_credentials": "Credenciais inv\u00e1lidas."
},
"step": { "step": {
"user": { "user": {
"data": { "data": {
"password": "Senha",
"username": "Endere\u00e7o de e-mail" "username": "Endere\u00e7o de e-mail"
} }
} }

View File

@ -20,10 +20,17 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import ATTRIBUTION, DOMAIN, DEFAULT_CACHEDB from .const import (
ATTRIBUTION,
DOMAIN,
DEFAULT_CACHEDB,
SIGNAL_CAPTURE_IMAGE,
SIGNAL_TRIGGER_QUICK_ACTION,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -89,7 +96,7 @@ class AbodeSystem:
self.abode = abode self.abode = abode
self.polling = polling self.polling = polling
self.devices = [] self.entity_ids = set()
self.logout_listener = None self.logout_listener = None
@ -179,27 +186,29 @@ def setup_hass_services(hass):
"""Capture a new image.""" """Capture a new image."""
entity_ids = call.data.get(ATTR_ENTITY_ID) entity_ids = call.data.get(ATTR_ENTITY_ID)
target_devices = [ target_entities = [
device entity_id
for device in hass.data[DOMAIN].devices for entity_id in hass.data[DOMAIN].entity_ids
if device.entity_id in entity_ids if entity_id in entity_ids
] ]
for device in target_devices: for entity_id in target_entities:
device.capture() signal = SIGNAL_CAPTURE_IMAGE.format(entity_id)
dispatcher_send(hass, signal)
def trigger_quick_action(call): def trigger_quick_action(call):
"""Trigger a quick action.""" """Trigger a quick action."""
entity_ids = call.data.get(ATTR_ENTITY_ID, None) entity_ids = call.data.get(ATTR_ENTITY_ID, None)
target_devices = [ target_entities = [
device entity_id
for device in hass.data[DOMAIN].devices for entity_id in hass.data[DOMAIN].entity_ids
if device.entity_id in entity_ids if entity_id in entity_ids
] ]
for device in target_devices: for entity_id in target_entities:
device.trigger() signal = SIGNAL_TRIGGER_QUICK_ACTION.format(entity_id)
dispatcher_send(hass, signal)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
@ -290,6 +299,7 @@ class AbodeDevice(Entity):
self._device.device_id, self._device.device_id,
self._update_callback, self._update_callback,
) )
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe from device events.""" """Unsubscribe from device events."""
@ -352,13 +362,14 @@ class AbodeAutomation(Entity):
self._event = event self._event = event
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe Abode events.""" """Subscribe to a group of Abode timeline events."""
if self._event: if self._event:
self.hass.async_add_job( self.hass.async_add_job(
self._data.abode.events.add_event_callback, self._data.abode.events.add_event_callback,
self._event, self._event,
self._update_callback, self._update_callback,
) )
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
@property @property
def should_poll(self): def should_poll(self):

View File

@ -23,9 +23,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up an alarm control panel for an Abode device.""" """Set up Abode alarm control panel device."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
async_add_entities( async_add_entities(
[AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))]
) )

View File

@ -5,9 +5,10 @@ import abodepy.helpers.constants as CONST
import abodepy.helpers.timeline as TIMELINE import abodepy.helpers.timeline as TIMELINE
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import AbodeAutomation, AbodeDevice from . import AbodeAutomation, AbodeDevice
from .const import DOMAIN from .const import DOMAIN, SIGNAL_TRIGGER_QUICK_ACTION
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -18,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a sensor for an Abode device.""" """Set up Abode binary sensor devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
device_types = [ device_types = [
@ -29,19 +30,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
CONST.TYPE_OPENING, CONST.TYPE_OPENING,
] ]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=device_types): for device in data.abode.get_devices(generic_type=device_types):
devices.append(AbodeBinarySensor(data, device)) entities.append(AbodeBinarySensor(data, device))
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
devices.append( entities.append(
AbodeQuickActionBinarySensor( AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
) )
) )
async_add_entities(devices) async_add_entities(entities)
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
@ -61,6 +62,12 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice): class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice):
"""A binary sensor implementation for Abode quick action automations.""" """A binary sensor implementation for Abode quick action automations."""
async def async_added_to_hass(self):
"""Subscribe Abode events."""
await super().async_added_to_hass()
signal = SIGNAL_TRIGGER_QUICK_ACTION.format(self.entity_id)
async_dispatcher_connect(self.hass, signal, self.trigger)
def trigger(self): def trigger(self):
"""Trigger a quick automation.""" """Trigger a quick automation."""
self._automation.trigger() self._automation.trigger()

View File

@ -7,10 +7,11 @@ import abodepy.helpers.timeline as TIMELINE
import requests import requests
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import Throttle from homeassistant.util import Throttle
from . import AbodeDevice from . import AbodeDevice
from .const import DOMAIN from .const import DOMAIN, SIGNAL_CAPTURE_IMAGE
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
@ -23,15 +24,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a camera for an Abode device.""" """Set up Abode camera devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA):
devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE))
async_add_entities(devices) for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA):
entities.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE))
async_add_entities(entities)
class AbodeCamera(AbodeDevice, Camera): class AbodeCamera(AbodeDevice, Camera):
@ -54,6 +55,9 @@ class AbodeCamera(AbodeDevice, Camera):
self._capture_callback, self._capture_callback,
) )
signal = SIGNAL_CAPTURE_IMAGE.format(self.entity_id)
async_dispatcher_connect(self.hass, signal, self.capture)
def capture(self): def capture(self):
"""Request a new image capture.""" """Request a new image capture."""
return self._device.capture() return self._device.capture()

View File

@ -3,3 +3,6 @@ DOMAIN = "abode"
ATTRIBUTION = "Data provided by goabode.com" ATTRIBUTION = "Data provided by goabode.com"
DEFAULT_CACHEDB = "abodepy_cache.pickle" DEFAULT_CACHEDB = "abodepy_cache.pickle"
SIGNAL_CAPTURE_IMAGE = "abode_camera_capture_{}"
SIGNAL_TRIGGER_QUICK_ACTION = "abode_trigger_quick_action_{}"

View File

@ -18,14 +18,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode cover devices.""" """Set up Abode cover devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER):
devices.append(AbodeCover(data, device))
async_add_entities(devices) for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER):
entities.append(AbodeCover(data, device))
async_add_entities(entities)
class AbodeCover(AbodeDevice, CoverDevice): class AbodeCover(AbodeDevice, CoverDevice):

View File

@ -33,12 +33,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode light devices.""" """Set up Abode light devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT):
devices.append(AbodeLight(data, device)) entities.append(AbodeLight(data, device))
async_add_entities(devices) async_add_entities(entities)
class AbodeLight(AbodeDevice, Light): class AbodeLight(AbodeDevice, Light):

View File

@ -18,14 +18,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode lock devices.""" """Set up Abode lock devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK):
devices.append(AbodeLock(data, device))
async_add_entities(devices) for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK):
entities.append(AbodeLock(data, device))
async_add_entities(entities)
class AbodeLock(AbodeDevice, LockDevice): class AbodeLock(AbodeDevice, LockDevice):

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/abode", "documentation": "https://www.home-assistant.io/integrations/abode",
"requirements": [ "requirements": [
"abodepy==0.16.6" "abodepy==0.16.7"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -16,9 +16,9 @@ _LOGGER = logging.getLogger(__name__)
# Sensor types: Name, icon # Sensor types: Name, icon
SENSOR_TYPES = { SENSOR_TYPES = {
"temp": ["Temperature", DEVICE_CLASS_TEMPERATURE], CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE],
"humidity": ["Humidity", DEVICE_CLASS_HUMIDITY], CONST.HUMI_STATUS_KEY: ["Humidity", DEVICE_CLASS_HUMIDITY],
"lux": ["Lux", DEVICE_CLASS_ILLUMINANCE], CONST.LUX_STATUS_KEY: ["Lux", DEVICE_CLASS_ILLUMINANCE],
} }
@ -28,16 +28,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a sensor for an Abode device.""" """Set up Abode sensor devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR):
for sensor_type in SENSOR_TYPES: for sensor_type in SENSOR_TYPES:
devices.append(AbodeSensor(data, device, sensor_type)) if sensor_type not in device.get_value(CONST.STATUSES_KEY):
continue
entities.append(AbodeSensor(data, device, sensor_type))
async_add_entities(devices) async_add_entities(entities)
class AbodeSensor(AbodeDevice): class AbodeSensor(AbodeDevice):
@ -62,6 +64,11 @@ class AbodeSensor(AbodeDevice):
"""Return the device class.""" """Return the device class."""
return self._device_class return self._device_class
@property
def unique_id(self):
"""Return a unique ID to use for this device."""
return f"{self._device.device_uuid}-{self._sensor_type}"
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""

View File

@ -21,17 +21,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode switch devices.""" """Set up Abode switch devices."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
devices = [] entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH):
devices.append(AbodeSwitch(data, device)) entities.append(AbodeSwitch(data, device))
for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION):
devices.append( entities.append(
AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)
) )
async_add_entities(devices) async_add_entities(entities)
class AbodeSwitch(AbodeDevice, SwitchDevice): class AbodeSwitch(AbodeDevice, SwitchDevice):

View File

@ -1,6 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"adguard_home_addon_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u0437\u0430 Hass.io AdGuard Home.",
"adguard_home_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}.",
"existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.",
"single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home." "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home."
}, },

View File

@ -1,6 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.",
"adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.",
"existing_instance_updated": "Configurazione esistente aggiornata.", "existing_instance_updated": "Configurazione esistente aggiornata.",
"single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home."
}, },

View File

@ -0,0 +1,22 @@
{
"config": {
"error": {
"auth": "API \u043a\u043b\u044e\u0447\u044a\u0442 \u043d\u0435 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.",
"name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430.",
"wrong_location": "\u0412 \u0442\u0430\u0437\u0438 \u043e\u0431\u043b\u0430\u0441\u0442 \u043d\u044f\u043c\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u0442\u0435\u043b\u043d\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Airly."
},
"step": {
"user": {
"data": {
"api_key": "API \u043a\u043b\u044e\u0447 \u0437\u0430 Airly",
"latitude": "\u0428\u0438\u0440\u0438\u043d\u0430",
"longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430",
"name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u0432\u044a\u0437\u0434\u0443\u0445\u0430 Airly \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u043b\u044e\u0447 \u0437\u0430 API, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 https://developer.airly.eu/register",
"title": "Airly"
}
},
"title": "Airly"
}
}

View File

@ -0,0 +1,11 @@
{
"device_automation": {
"action_type": {
"arm_away": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0438\u0435",
"arm_home": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u0432\u043a\u044a\u0449\u0438",
"arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c",
"disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}",
"trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}"
}
}
}

View File

@ -0,0 +1,11 @@
{
"device_automation": {
"action_type": {
"arm_away": "Aktivovat {entity_name} v re\u017eimu mimo domov",
"arm_home": "Aktivovat {entity_name} v re\u017eimu doma",
"arm_night": "Aktivovat {entity_name} v re\u017eimu noc",
"disarm": "Deaktivovat {entity_name}",
"trigger": "Spustit {entity_name}"
}
}
}

View File

@ -1,6 +1,9 @@
{ {
"device_automation": { "device_automation": {
"action_type": { "action_type": {
"arm_away": "Inschakelen {entity_name} afwezig",
"arm_home": "Inschakelen {entity_name} thuis",
"arm_night": "Inschakelen {entity_name} nacht",
"disarm": "Uitschakelen {entity_name}", "disarm": "Uitschakelen {entity_name}",
"trigger": "Trigger {entity_name}" "trigger": "Trigger {entity_name}"
} }

View File

@ -1,6 +1,10 @@
{ {
"device_automation": { "device_automation": {
"action_type": { "action_type": {
"arm_away": "Armar {entity_name} longe",
"arm_home": "Armar {entity_name} casa",
"arm_night": "Armar {entity_name} noite",
"disarm": "Desarmar {entity_name}",
"trigger": "Disparar {entidade_nome}" "trigger": "Disparar {entidade_nome}"
} }
} }

View File

@ -1,6 +1,10 @@
{ {
"device_automation": { "device_automation": {
"action_type": { "action_type": {
"arm_away": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
"arm_home": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
"arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
"disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
"trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442"
} }
} }

View File

@ -0,0 +1,84 @@
"""Reproduce an Alarm control panel state."""
import asyncio
import logging
from typing import Iterable, Optional
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
SERVICE_ALARM_ARM_HOME,
SERVICE_ALARM_ARM_NIGHT,
SERVICE_ALARM_DISARM,
SERVICE_ALARM_TRIGGER,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import Context, State
from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
VALID_STATES = {
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
}
async def _async_reproduce_state(
hass: HomeAssistantType, state: State, context: Optional[Context] = None
) -> None:
"""Reproduce a single state."""
cur_state = hass.states.get(state.entity_id)
if cur_state is None:
_LOGGER.warning("Unable to find entity %s", state.entity_id)
return
if state.state not in VALID_STATES:
_LOGGER.warning(
"Invalid state specified for %s: %s", state.entity_id, state.state
)
return
# Return if we are already at the right state.
if cur_state.state == state.state:
return
service_data = {ATTR_ENTITY_ID: state.entity_id}
if state.state == STATE_ALARM_ARMED_AWAY:
service = SERVICE_ALARM_ARM_AWAY
elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS:
service = SERVICE_ALARM_ARM_CUSTOM_BYPASS
elif state.state == STATE_ALARM_ARMED_HOME:
service = SERVICE_ALARM_ARM_HOME
elif state.state == STATE_ALARM_ARMED_NIGHT:
service = SERVICE_ALARM_ARM_NIGHT
elif state.state == STATE_ALARM_DISARMED:
service = SERVICE_ALARM_DISARM
elif state.state == STATE_ALARM_TRIGGERED:
service = SERVICE_ALARM_TRIGGER
await hass.services.async_call(
DOMAIN, service, service_data, context=context, blocking=True
)
async def async_reproduce_states(
hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None
) -> None:
"""Reproduce Alarm control panel states."""
await asyncio.gather(
*(_async_reproduce_state(hass, state, context) for state in states)
)

View File

@ -10,6 +10,16 @@ alarm_disarm:
description: An optional code to disarm the alarm control panel with. description: An optional code to disarm the alarm control panel with.
example: 1234 example: 1234
alarm_arm_custom_bypass:
description: Send arm custom bypass command.
fields:
entity_id:
description: Name of alarm control panel to arm custom bypass.
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to arm custom bypass the alarm control panel with.
example: 1234
alarm_arm_home: alarm_arm_home:
description: Send the alarm the command for arm home. description: Send the alarm the command for arm home.
fields: fields:

View File

@ -12,11 +12,14 @@ from homeassistant.const import (
STATE_LOCKED, STATE_LOCKED,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNLOCKED,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_UNLOCKED,
) )
import homeassistant.components.climate.const as climate import homeassistant.components.climate.const as climate
import homeassistant.components.media_player.const as media_player
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
from homeassistant.components import light, fan, cover from homeassistant.components import light, fan, cover
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
@ -110,6 +113,11 @@ class AlexaCapability:
"""Return the Configuration object.""" """Return the Configuration object."""
return [] return []
@staticmethod
def supported_operations():
"""Return the supportedOperations object."""
return []
def serialize_discovery(self): def serialize_discovery(self):
"""Serialize according to the Discovery API.""" """Serialize according to the Discovery API."""
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
@ -150,6 +158,10 @@ class AlexaCapability:
if instance is not None: if instance is not None:
result["instance"] = instance result["instance"] = instance
supported_operations = self.supported_operations()
if supported_operations:
result["supportedOperations"] = supported_operations
return result return result
def serialize_properties(self): def serialize_properties(self):
@ -484,6 +496,28 @@ class AlexaPlaybackController(AlexaCapability):
"""Return the Alexa API name of this interface.""" """Return the Alexa API name of this interface."""
return "Alexa.PlaybackController" return "Alexa.PlaybackController"
def supported_operations(self):
"""Return the supportedOperations object.
Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop
"""
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
operations = {
media_player.SUPPORT_NEXT_TRACK: "Next",
media_player.SUPPORT_PAUSE: "Pause",
media_player.SUPPORT_PLAY: "Play",
media_player.SUPPORT_PREVIOUS_TRACK: "Previous",
media_player.SUPPORT_STOP: "Stop",
}
supported_operations = []
for operation in operations:
if operation & supported_features:
supported_operations.append(operations[operation])
return supported_operations
class AlexaInputController(AlexaCapability): class AlexaInputController(AlexaCapability):
"""Implements Alexa.InputController. """Implements Alexa.InputController.
@ -704,6 +738,33 @@ class AlexaThermostatController(AlexaCapability):
return {"value": temp, "scale": API_TEMP_UNITS[unit]} return {"value": temp, "scale": API_TEMP_UNITS[unit]}
def configuration(self):
"""Return configuration object.
Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values.
ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM.
"""
supported_modes = []
hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES)
for mode in hvac_modes:
thermostat_mode = API_THERMOSTAT_MODES.get(mode)
if thermostat_mode:
supported_modes.append(thermostat_mode)
preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES)
for mode in preset_modes:
thermostat_mode = API_THERMOSTAT_PRESETS.get(mode)
if thermostat_mode:
supported_modes.append(thermostat_mode)
# Return False for supportsScheduling until supported with event listener in handler.
configuration = {"supportsScheduling": False}
if supported_modes:
configuration["supportedModes"] = supported_modes
return configuration
class AlexaPowerLevelController(AlexaCapability): class AlexaPowerLevelController(AlexaCapability):
"""Implements Alexa.PowerLevelController. """Implements Alexa.PowerLevelController.
@ -1078,3 +1139,50 @@ class AlexaDoorbellEventSource(AlexaCapability):
def capability_proactively_reported(self): def capability_proactively_reported(self):
"""Return True for proactively reported capability.""" """Return True for proactively reported capability."""
return True return True
class AlexaPlaybackStateReporter(AlexaCapability):
"""Implements Alexa.PlaybackStateReporter.
https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return "Alexa.PlaybackStateReporter"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{"name": "playbackState"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
return True
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
return True
def get_property(self, name):
"""Read and return a property."""
if name != "playbackState":
raise UnsupportedProperty(name)
playback_state = self.entity.state
if playback_state == STATE_PLAYING:
return {"state": "PLAYING"}
if playback_state == STATE_PAUSED:
return {"state": "PAUSED"}
return {"state": "STOPPED"}
class AlexaSeekController(AlexaCapability):
"""Implements Alexa.SeekController.
https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html
"""
def name(self):
"""Return the Alexa API name of this interface."""
return "Alexa.SeekController"

View File

@ -56,9 +56,10 @@ API_THERMOSTAT_MODES = OrderedDict(
(climate.HVAC_MODE_AUTO, "AUTO"), (climate.HVAC_MODE_AUTO, "AUTO"),
(climate.HVAC_MODE_OFF, "OFF"), (climate.HVAC_MODE_OFF, "OFF"),
(climate.HVAC_MODE_FAN_ONLY, "OFF"), (climate.HVAC_MODE_FAN_ONLY, "OFF"),
(climate.HVAC_MODE_DRY, "OFF"), (climate.HVAC_MODE_DRY, "CUSTOM"),
] ]
) )
API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"}
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
PERCENTAGE_FAN_MAP = { PERCENTAGE_FAN_MAP = {

View File

@ -46,11 +46,13 @@ from .capabilities import (
AlexaMotionSensor, AlexaMotionSensor,
AlexaPercentageController, AlexaPercentageController,
AlexaPlaybackController, AlexaPlaybackController,
AlexaPlaybackStateReporter,
AlexaPowerController, AlexaPowerController,
AlexaPowerLevelController, AlexaPowerLevelController,
AlexaRangeController, AlexaRangeController,
AlexaSceneController, AlexaSceneController,
AlexaSecurityPanelController, AlexaSecurityPanelController,
AlexaSeekController,
AlexaSpeaker, AlexaSpeaker,
AlexaStepSpeaker, AlexaStepSpeaker,
AlexaTemperatureSensor, AlexaTemperatureSensor,
@ -391,6 +393,10 @@ class MediaPlayerCapabilities(AlexaEntity):
def default_display_categories(self): def default_display_categories(self):
"""Return the display categories for this entity.""" """Return the display categories for this entity."""
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
if device_class == media_player.DEVICE_CLASS_SPEAKER:
return [DisplayCategory.SPEAKER]
return [DisplayCategory.TV] return [DisplayCategory.TV]
def interfaces(self): def interfaces(self):
@ -418,6 +424,10 @@ class MediaPlayerCapabilities(AlexaEntity):
) )
if supported & playback_features: if supported & playback_features:
yield AlexaPlaybackController(self.entity) yield AlexaPlaybackController(self.entity)
yield AlexaPlaybackStateReporter(self.entity)
if supported & media_player.const.SUPPORT_SEEK:
yield AlexaSeekController(self.entity)
if supported & media_player.SUPPORT_SELECT_SOURCE: if supported & media_player.SUPPORT_SELECT_SOURCE:
yield AlexaInputController(self.entity) yield AlexaInputController(self.entity)

View File

@ -111,3 +111,10 @@ class AlexaInvalidDirectiveError(AlexaError):
namespace = "Alexa" namespace = "Alexa"
error_type = "INVALID_DIRECTIVE" error_type = "INVALID_DIRECTIVE"
class AlexaVideoActionNotPermittedForContentError(AlexaError):
"""Class to represent action not permitted for content errors."""
namespace = "Alexa.Video"
error_type = "ACTION_NOT_PERMITTED_FOR_CONTENT"

View File

@ -38,6 +38,7 @@ from homeassistant.util.temperature import convert as convert_temperature
from .const import ( from .const import (
API_TEMP_UNITS, API_TEMP_UNITS,
API_THERMOSTAT_MODES_CUSTOM,
API_THERMOSTAT_MODES, API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS, API_THERMOSTAT_PRESETS,
Cause, Cause,
@ -53,6 +54,7 @@ from .errors import (
AlexaSecurityPanelUnauthorizedError, AlexaSecurityPanelUnauthorizedError,
AlexaTempRangeError, AlexaTempRangeError,
AlexaUnsupportedThermostatModeError, AlexaUnsupportedThermostatModeError,
AlexaVideoActionNotPermittedForContentError,
) )
from .state_report import async_enable_proactive_mode from .state_report import async_enable_proactive_mode
@ -767,11 +769,28 @@ async def async_api_set_thermostat_mode(hass, config, directive, context):
raise AlexaUnsupportedThermostatModeError(msg) raise AlexaUnsupportedThermostatModeError(msg)
service = climate.SERVICE_SET_PRESET_MODE service = climate.SERVICE_SET_PRESET_MODE
data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO data[climate.ATTR_PRESET_MODE] = ha_preset
elif mode == "CUSTOM":
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
custom_mode = directive.payload["thermostatMode"]["customName"]
custom_mode = next(
(k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode),
None,
)
if custom_mode not in operation_list:
msg = (
f"The requested thermostat mode {mode}: {custom_mode} is not supported"
)
raise AlexaUnsupportedThermostatModeError(msg)
service = climate.SERVICE_SET_HVAC_MODE
data[climate.ATTR_HVAC_MODE] = custom_mode
else: else:
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode}
ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None)
if ha_mode not in operation_list: if ha_mode not in operation_list:
msg = f"The requested thermostat mode {mode} is not supported" msg = f"The requested thermostat mode {mode} is not supported"
raise AlexaUnsupportedThermostatModeError(msg) raise AlexaUnsupportedThermostatModeError(msg)
@ -1168,3 +1187,45 @@ async def async_api_skipchannel(hass, config, directive, context):
) )
return response return response
@HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition"))
async def async_api_seek(hass, config, directive, context):
"""Process a seek request."""
entity = directive.entity
position_delta = int(directive.payload["deltaPositionMilliseconds"])
current_position = entity.attributes.get(media_player.ATTR_MEDIA_POSITION)
if not current_position:
msg = f"{entity} did not return the current media position."
raise AlexaVideoActionNotPermittedForContentError(msg)
seek_position = int(current_position) + int(position_delta / 1000)
if seek_position < 0:
seek_position = 0
media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION)
if media_duration and 0 < int(media_duration) < seek_position:
seek_position = media_duration
data = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.ATTR_MEDIA_SEEK_POSITION: seek_position,
}
await hass.services.async_call(
media_player.DOMAIN,
media_player.SERVICE_MEDIA_SEEK,
data,
blocking=False,
context=context,
)
# convert seconds to milliseconds for StateReport.
seek_position = int(seek_position * 1000)
payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]}
return directive.response(
name="StateReport", namespace="Alexa.SeekController", payload=payload
)

View File

@ -0,0 +1,9 @@
{
"config": {
"abort": {
"already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.",
"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."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,9 @@
{
"config": {
"abort": {
"already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.",
"cannot_connect": "No es pot connectar amb el servidor d'Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,9 @@
{
"config": {
"abort": {
"already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.",
"cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"already_setup": "You can only configure one Almond account.",
"cannot_connect": "Unable to connect to the Almond server.",
"missing_configuration": "Please check the documentation on how to set up Almond."
},
"step": {
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"title": "Almond"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"already_setup": "S\u00f3lo puede configurar una cuenta de Almond.",
"cannot_connect": "No se puede conectar al servidor Almond.",
"missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond."
},
"step": {
"pick_implementation": {
"title": "Seleccione el m\u00e9todo de autenticaci\u00f3n"
}
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "Vous ne pouvez configurer qu'un seul compte Almond",
"cannot_connect": "Impossible de se connecter au serveur Almond",
"missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "\u00c8 possibile configurare un solo account Almond.",
"cannot_connect": "Impossibile connettersi al server Almond.",
"missing_configuration": "Si prega di controllare la documentazione su come impostare Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,9 @@
{
"config": {
"abort": {
"already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.",
"cannot_connect": "Kann sech net mam Almond Server verbannen.",
"missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "U kunt slechts \u00e9\u00e9n Almond-account configureren.",
"cannot_connect": "Kan geen verbinding maken met de Almond-server.",
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "Du kan bare konfigurere en Almond konto.",
"cannot_connect": "Kan ikke koble til Almond-serveren.",
"missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,9 @@
{
"config": {
"abort": {
"already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Almond.",
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond.",
"missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "Konfigurirate lahko samo en ra\u010dun Almond.",
"cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.",
"missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond."
},
"title": "Almond"
}
}

View File

@ -0,0 +1,10 @@
{
"config": {
"abort": {
"already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002",
"cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002",
"missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002"
},
"title": "Almond"
}
}

View File

@ -0,0 +1,308 @@
"""Support for Almond."""
import asyncio
from datetime import timedelta
import logging
import time
from typing import Optional
import async_timeout
from aiohttp import ClientSession, ClientError
from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI
import voluptuous as vol
from homeassistant.core import HomeAssistant, CoreState
from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.helpers import (
config_validation as cv,
config_entry_oauth2_flow,
event,
intent,
aiohttp_client,
storage,
network,
)
from homeassistant import config_entries
from homeassistant.components import conversation
from . import config_flow
from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2
CONF_CLIENT_ID = "client_id"
CONF_CLIENT_SECRET = "client_secret"
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, config):
"""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": config_entries.SOURCE_IMPORT},
data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]},
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
"""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: config_entries.ConfigEntry, api: WebAlmondAPI
):
"""Configure Almond to connect to HA."""
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.async_get_external_url(hass)
else:
hass_url = hass.config.api.base_url
# If hass_url is None, we're not going to configure Almond to connect to HA.
if hass_url is None:
return
_LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url)
store = storage.Store(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_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:
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 = "Request timeout"
else:
msg = err
_LOGGER.warning("Unable to configure Almond: %s", msg)
await hass.auth.async_remove_refresh_token(refresh_token)
raise ConfigEntryNotReady
# 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, entry):
"""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,
):
"""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: config_entries.ConfigEntry
):
"""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, conversation_id: Optional[str] = None
) -> intent.IntentResponse:
"""Process a sentence."""
response = await self.api.async_converse_text(text, conversation_id)
first_choice = True
buffer = ""
for message in response["messages"]:
if message["type"] == "text":
buffer += "\n" + message["text"]
elif message["type"] == "picture":
buffer += "\n Picture: " + message["url"]
elif message["type"] == "rdl":
buffer += (
"\n Link: "
+ message["rdl"]["displayTitle"]
+ " "
+ message["rdl"]["webCallback"]
)
elif message["type"] == "choice":
if first_choice:
first_choice = False
else:
buffer += ","
buffer += f" {message['title']}"
intent_result = intent.IntentResponse()
intent_result.async_set_speech(buffer.strip())
return intent_result

View File

@ -0,0 +1,125 @@
"""Config flow to connect with Home Assistant."""
import asyncio
import logging
import async_timeout
from aiohttp import ClientError
from yarl import URL
import voluptuous as vol
from pyalmond import AlmondLocalAuth, WebAlmondAPI
from homeassistant import data_entry_flow, config_entries, core
from homeassistant.helpers import config_entry_oauth2_flow, aiohttp_client
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:
with async_timeout.timeout(10):
await api.async_list_apps()
return True
except (asyncio.TimeoutError, ClientError):
return False
@config_entries.HANDLERS.register(DOMAIN)
class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
"""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="already_setup")
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.RESULT_TYPE_EXTERNAL_STEP:
self.host = str(URL(result["url"]).with_path("me"))
return result
async def async_oauth_create_entry(self, data: dict) -> dict:
"""Create an entry for the flow.
Ok to override if you want to fetch extra info or even add another step.
"""
# pylint: disable=invalid-name
self.CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
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 = None) -> dict:
"""Import data."""
# Only allow 1 instance.
if self._async_current_entries():
return self.async_abort(reason="already_setup")
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")
# pylint: disable=invalid-name
self.CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
return self.async_create_entry(
title="Configuration.yaml",
data={"type": TYPE_LOCAL, "host": user_input["host"]},
)
async def async_step_hassio(self, user_input=None):
"""Receive a Hass.io discovery."""
if self._async_current_entries():
return self.async_abort(reason="already_setup")
self.hassio_discovery = user_input
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"]},
data_schema=vol.Schema({}),
)

View File

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

View File

@ -0,0 +1,9 @@
{
"domain": "almond",
"name": "Almond",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/almond",
"dependencies": ["http", "conversation"],
"codeowners": ["@gcampax", "@balloob"],
"requirements": ["pyalmond==0.0.2"]
}

View File

@ -0,0 +1,15 @@
{
"config": {
"step": {
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"already_setup": "You can only configure one Almond account.",
"cannot_connect": "Unable to connect to the Almond server.",
"missing_configuration": "Please check the documentation on how to set up Almond."
},
"title": "Almond"
}
}

View File

@ -3,10 +3,10 @@
"name": "Amazon polly", "name": "Amazon polly",
"documentation": "https://www.home-assistant.io/integrations/amazon_polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"requirements": [ "requirements": [
"boto3==1.9.233" "boto3==1.9.252"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [
"@robbiet480" "@robbiet480"
] ]
} }

View File

@ -145,7 +145,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
) )
def get_engine(hass, config): def get_engine(hass, config, discovery_info=None):
"""Set up Amazon Polly speech component.""" """Set up Amazon Polly speech component."""
output_format = config.get(CONF_OUTPUT_FORMAT) output_format = config.get(CONF_OUTPUT_FORMAT)
sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])

View File

@ -3,7 +3,7 @@
"name": "Androidtv", "name": "Androidtv",
"documentation": "https://www.home-assistant.io/integrations/androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [ "requirements": [
"adb-shell==0.0.7", "adb-shell==0.0.8",
"androidtv==0.0.32" "androidtv==0.0.32"
], ],
"dependencies": [], "dependencies": [],

View File

@ -287,8 +287,11 @@ class ADBDevice(MediaPlayerDevice):
"""Initialize the Android TV / Fire TV device.""" """Initialize the Android TV / Fire TV device."""
self.aftv = aftv self.aftv = aftv
self._name = name self._name = name
self._apps = APPS.copy() self._app_id_to_name = APPS.copy()
self._apps.update(apps) self._app_id_to_name.update(apps)
self._app_name_to_id = {
value: key for key, value in self._app_id_to_name.items()
}
self._keys = KEYS self._keys = KEYS
self._device_properties = self.aftv.device_properties self._device_properties = self.aftv.device_properties
@ -328,7 +331,7 @@ class ADBDevice(MediaPlayerDevice):
@property @property
def app_name(self): def app_name(self):
"""Return the friendly name of the current app.""" """Return the friendly name of the current app."""
return self._apps.get(self._current_app, self._current_app) return self._app_id_to_name.get(self._current_app, self._current_app)
@property @property
def available(self): def available(self):
@ -455,9 +458,13 @@ class AndroidTVDevice(ADBDevice):
return return
# Get the updated state and attributes. # Get the updated state and attributes.
state, self._current_app, self._device, self._is_volume_muted, self._volume_level = ( (
self.aftv.update() state,
) self._current_app,
self._device,
self._is_volume_muted,
self._volume_level,
) = self.aftv.update()
self._state = ANDROIDTV_STATES.get(state) self._state = ANDROIDTV_STATES.get(state)
if self._state is None: if self._state is None:
@ -514,7 +521,7 @@ class FireTVDevice(ADBDevice):
super().__init__(aftv, name, apps, turn_on_command, turn_off_command) super().__init__(aftv, name, apps, turn_on_command, turn_off_command)
self._get_sources = get_sources self._get_sources = get_sources
self._running_apps = None self._sources = None
@adb_decorator(override_available=True) @adb_decorator(override_available=True)
def update(self): def update(self):
@ -534,23 +541,28 @@ class FireTVDevice(ADBDevice):
return return
# Get the `state`, `current_app`, and `running_apps`. # Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._running_apps = self.aftv.update( state, self._current_app, running_apps = self.aftv.update(self._get_sources)
self._get_sources
)
self._state = ANDROIDTV_STATES.get(state) self._state = ANDROIDTV_STATES.get(state)
if self._state is None: if self._state is None:
self._available = False self._available = False
if running_apps:
self._sources = [
self._app_id_to_name.get(app_id, app_id) for app_id in running_apps
]
else:
self._sources = None
@property @property
def source(self): def source(self):
"""Return the current app.""" """Return the current app."""
return self._current_app return self._app_id_to_name.get(self._current_app, self._current_app)
@property @property
def source_list(self): def source_list(self):
"""Return a list of running apps.""" """Return a list of running apps."""
return self._running_apps return self._sources
@property @property
def supported_features(self): def supported_features(self):
@ -571,6 +583,7 @@ class FireTVDevice(ADBDevice):
""" """
if isinstance(source, str): if isinstance(source, str):
if not source.startswith("!"): if not source.startswith("!"):
self.aftv.launch_app(source) self.aftv.launch_app(self._app_name_to_id.get(source, source))
else: else:
self.aftv.stop_app(source[1:].lstrip()) source_ = source[1:].lstrip()
self.aftv.stop_app(self._app_name_to_id.get(source_, source_))

View File

@ -0,0 +1,4 @@
# Describes the format for available arlo services
update:
description: Update the state for all cameras and the base station.

View File

@ -3,7 +3,7 @@
"name": "Asuswrt", "name": "Asuswrt",
"documentation": "https://www.home-assistant.io/integrations/asuswrt", "documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": [ "requirements": [
"aioasuswrt==1.1.21" "aioasuswrt==1.1.22"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -25,7 +25,7 @@
}, },
"step": { "step": {
"init": { "init": {
"description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
"title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
} }
}, },

View File

@ -4,7 +4,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant import exceptions from homeassistant import exceptions
from homeassistant.core import callback from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.const import ( from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
CONF_PLATFORM, CONF_PLATFORM,
@ -17,7 +17,8 @@ from homeassistant.helpers.event import async_track_state_change, async_track_sa
from homeassistant.helpers import condition, config_validation as cv, template from homeassistant.helpers import condition, config_validation as cv, template
# mypy: allow-untyped-defs, no-check-untyped-defs # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs
TRIGGER_SCHEMA = vol.All( TRIGGER_SCHEMA = vol.All(
vol.Schema( vol.Schema(
@ -42,7 +43,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_attach_trigger( async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="numeric_state" hass, config, action, automation_info, *, platform_type="numeric_state"
): ) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)
@ -52,7 +53,7 @@ async def async_attach_trigger(
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
unsub_track_same = {} unsub_track_same = {}
entities_triggered = set() entities_triggered = set()
period = {} period: dict = {}
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass

View File

@ -27,8 +27,8 @@ TRIGGER_SCHEMA = vol.All(
vol.Required(CONF_PLATFORM): "state", vol.Required(CONF_PLATFORM): "state",
vol.Required(CONF_ENTITY_ID): cv.entity_ids, vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions # These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): str, vol.Optional(CONF_FROM): vol.Any(str, [str]),
vol.Optional(CONF_TO): str, vol.Optional(CONF_TO): vol.Any(str, [str]),
vol.Optional(CONF_FOR): vol.Any( vol.Optional(CONF_FOR): vol.Any(
vol.All(cv.time_period, cv.positive_timedelta), vol.All(cv.time_period, cv.positive_timedelta),
cv.template, cv.template,

View File

@ -28,7 +28,9 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema(
) )
async def async_attach_trigger(hass, config, action, automation_info): async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="numeric_state"
):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass value_template.hass = hass
@ -65,7 +67,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
variables = { variables = {
"trigger": { "trigger": {
"platform": "template", "platform": platform_type,
"entity_id": entity_id, "entity_id": entity_id,
"from_state": from_s, "from_state": from_s,
"to_state": to_s, "to_state": to_s,

View File

@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/integrations/avea", "documentation": "https://www.home-assistant.io/integrations/avea",
"dependencies": [], "dependencies": [],
"codeowners": ["@pattyland"], "codeowners": ["@pattyland"],
"requirements": ["avea==1.2.8"] "requirements": ["avea==1.4"]
} }

View File

@ -3,7 +3,7 @@
"name": "Aws", "name": "Aws",
"documentation": "https://www.home-assistant.io/integrations/aws", "documentation": "https://www.home-assistant.io/integrations/aws",
"requirements": [ "requirements": [
"aiobotocore==0.10.2" "aiobotocore==0.10.4"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -12,6 +12,7 @@
"device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u043e", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u043e",
"faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438"
}, },
"flow_title": "Axis \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name} ({host})",
"step": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -0,0 +1,5 @@
{
"config": {
"flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})"
}
}

View File

@ -12,6 +12,7 @@
"device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel", "device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel",
"faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas" "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas"
}, },
"flow_title": "Eixos do dispositivo: {name} ({host})",
"step": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -0,0 +1 @@
"""The Azure Service Bus integration."""

View File

@ -0,0 +1,12 @@
{
"domain": "azure_service_bus",
"name": "Azure Service Bus",
"documentation": "https://www.home-assistant.io/integrations/azure_service_bus",
"requirements": [
"azure-servicebus==0.50.1"
],
"dependencies": [],
"codeowners": [
"@hfurubotten"
]
}

View File

@ -0,0 +1,106 @@
"""Support for azure service bus notification."""
import json
import logging
from azure.servicebus.aio import Message, ServiceBusClient
from azure.servicebus.common.errors import (
MessageSendFailed,
ServiceBusConnectionError,
ServiceBusResourceNotFound,
)
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_TARGET,
ATTR_TITLE,
PLATFORM_SCHEMA,
BaseNotificationService,
)
from homeassistant.const import CONTENT_TYPE_JSON
import homeassistant.helpers.config_validation as cv
CONF_CONNECTION_STRING = "connection_string"
CONF_QUEUE_NAME = "queue"
CONF_TOPIC_NAME = "topic"
ATTR_ASB_MESSAGE = "message"
ATTR_ASB_TITLE = "title"
ATTR_ASB_TARGET = "target"
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_QUEUE_NAME, CONF_TOPIC_NAME),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_CONNECTION_STRING): cv.string,
vol.Exclusive(
CONF_QUEUE_NAME, "output", "Can only send to a queue or a topic."
): cv.string,
vol.Exclusive(
CONF_TOPIC_NAME, "output", "Can only send to a queue or a topic."
): cv.string,
}
),
)
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config, discovery_info=None):
"""Get the notification service."""
connection_string = config[CONF_CONNECTION_STRING]
queue_name = config.get(CONF_QUEUE_NAME)
topic_name = config.get(CONF_TOPIC_NAME)
# Library can do synchronous IO when creating the clients.
# Passes in loop here, but can't run setup on the event loop.
servicebus = ServiceBusClient.from_connection_string(
connection_string, loop=hass.loop
)
try:
if queue_name:
client = servicebus.get_queue(queue_name)
else:
client = servicebus.get_topic(topic_name)
except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err:
_LOGGER.error(
"Connection error while creating client for queue/topic '%s'. %s",
queue_name or topic_name,
err,
)
return None
return ServiceBusNotificationService(client)
class ServiceBusNotificationService(BaseNotificationService):
"""Implement the notification service for the service bus service."""
def __init__(self, client):
"""Initialize the service."""
self._client = client
async def async_send_message(self, message, **kwargs):
"""Send a message."""
dto = {ATTR_ASB_MESSAGE: message}
if ATTR_TITLE in kwargs:
dto[ATTR_ASB_TITLE] = kwargs[ATTR_TITLE]
if ATTR_TARGET in kwargs:
dto[ATTR_ASB_TARGET] = kwargs[ATTR_TARGET]
data = kwargs.get(ATTR_DATA)
if data:
dto.update(data)
queue_message = Message(json.dumps(dto))
queue_message.properties.content_type = CONTENT_TYPE_JSON
try:
await self._client.send(queue_message)
except MessageSendFailed as err:
_LOGGER.error(
"Could not send service bus notification to %s. %s",
self._client.name,
err,
)

View File

@ -52,7 +52,7 @@ _OPTIONS = {
SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME] SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME]
def get_engine(hass, config): def get_engine(hass, config, discovery_info=None):
"""Set up Baidu TTS component.""" """Set up Baidu TTS component."""
return BaiduTTSProvider(hass, config) return BaiduTTSProvider(hass, config)

View File

@ -53,6 +53,7 @@
"hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438",
"light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430",
"locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d",
"moist": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u0435\u043d",
"moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d",
"motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
"moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
@ -71,6 +72,7 @@
"not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445",
"not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438",
"not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442",
"not_opened": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d",
"not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d",
"not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430",
"not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430",

View File

@ -0,0 +1,8 @@
{
"device_automation": {
"trigger_type": {
"moist": "{entity_name} se navlh\u010dil",
"not_opened": "{entity_name} uzav\u0159eno"
}
}
}

View File

@ -86,7 +86,7 @@
"smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e",
"sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son",
"turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9",
"turned_on": "{entity_name} activ\u00e9", "turned_on": "{entity_name} est activ\u00e9",
"unsafe": "{entity_name} est devenu dangereux", "unsafe": "{entity_name} est devenu dangereux",
"vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations"
} }

View File

@ -46,13 +46,14 @@
}, },
"trigger_type": { "trigger_type": {
"bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony",
"closed": "{entity_name} be lett z\u00e1rva", "closed": "{entity_name} be lett csukva",
"cold": "{entity_name} hideg lett", "cold": "{entity_name} hideg lett",
"connected": "{entity_name} csatlakozott", "connected": "{entity_name} csatlakozik",
"gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel",
"hot": "{entity_name} felforr\u00f3sodott", "hot": "{entity_name} felforr\u00f3sodik",
"light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel",
"locked": "{entity_name} be lett z\u00e1rva", "locked": "{entity_name} be lett z\u00e1rva",
"moist": "{entity_name} nedves lett",
"moist\u00a7": "{entity_name} nedves lett", "moist\u00a7": "{entity_name} nedves lett",
"motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel",
"moving": "{entity_name} mozog", "moving": "{entity_name} mozog",
@ -65,12 +66,13 @@
"no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st",
"not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151",
"not_cold": "{entity_name} m\u00e1r nem hideg", "not_cold": "{entity_name} m\u00e1r nem hideg",
"not_connected": "{entity_name} lecsatlakozott", "not_connected": "{entity_name} lecsatlakozik",
"not_hot": "{entity_name} m\u00e1r nem forr\u00f3", "not_hot": "{entity_name} m\u00e1r nem forr\u00f3",
"not_locked": "{entity_name} ki lett nyitva", "not_locked": "{entity_name} ki lett nyitva",
"not_moist": "{entity_name} sz\u00e1raz lett", "not_moist": "{entity_name} sz\u00e1raz lett",
"not_moving": "{entity_name} m\u00e1r nem mozog", "not_moving": "{entity_name} m\u00e1r nem mozog",
"not_occupied": "{entity_name} m\u00e1r nem foglalt", "not_occupied": "{entity_name} m\u00e1r nem foglalt",
"not_opened": "{entity_name} be lett csukva",
"not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva",
"not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt",
"not_present": "{entity_name} m\u00e1r nincs jelen", "not_present": "{entity_name} m\u00e1r nincs jelen",

View File

@ -28,7 +28,7 @@
"is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
"is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
"is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
"is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435",
"is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
"is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
"is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
@ -36,7 +36,7 @@
"is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
"is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
"is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
"is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435",
"is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
"is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443",
"is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c",

View File

@ -1,6 +1,6 @@
"""Support for Bluesound devices.""" """Support for Bluesound devices."""
import asyncio import asyncio
from asyncio.futures import CancelledError from asyncio import CancelledError
from datetime import timedelta from datetime import timedelta
import logging import logging
from urllib import parse from urllib import parse
@ -53,6 +53,7 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_BLUESOUND_GROUP = "bluesound_group"
ATTR_MASTER = "master" ATTR_MASTER = "master"
DATA_BLUESOUND = "bluesound" DATA_BLUESOUND = "bluesound"
@ -219,6 +220,8 @@ class BluesoundPlayer(MediaPlayerDevice):
self._master = None self._master = None
self._is_master = False self._is_master = False
self._group_name = None self._group_name = None
self._group_list = []
self._bluesound_device_name = None
self._init_callback = init_callback self._init_callback = init_callback
if self.port is None: if self.port is None:
@ -247,6 +250,8 @@ class BluesoundPlayer(MediaPlayerDevice):
if not self._name: if not self._name:
self._name = self._sync_status.get("@name", self.host) self._name = self._sync_status.get("@name", self.host)
if not self._bluesound_device_name:
self._bluesound_device_name = self._sync_status.get("@name", self.host)
if not self._icon: if not self._icon:
self._icon = self._sync_status.get("@icon", self.host) self._icon = self._sync_status.get("@icon", self.host)
@ -331,7 +336,6 @@ class BluesoundPlayer(MediaPlayerDevice):
self, method, raise_timeout=False, allow_offline=False self, method, raise_timeout=False, allow_offline=False
): ):
"""Send command to the player.""" """Send command to the player."""
if not self._is_online and not allow_offline: if not self._is_online and not allow_offline:
return return
@ -371,7 +375,6 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_update_status(self): async def async_update_status(self):
"""Use the poll session to always get the status of the player.""" """Use the poll session to always get the status of the player."""
response = None response = None
url = "Status" url = "Status"
@ -402,6 +405,10 @@ class BluesoundPlayer(MediaPlayerDevice):
if group_name != self._group_name: if group_name != self._group_name:
_LOGGER.debug("Group name change detected on device: %s", self.host) _LOGGER.debug("Group name change detected on device: %s", self.host)
self._group_name = group_name self._group_name = group_name
# rebuild ordered list of entity_ids that are in the group, master is first
self._group_list = self.rebuild_bluesound_group()
# the sleep is needed to make sure that the # the sleep is needed to make sure that the
# devices is synced # devices is synced
await asyncio.sleep(1) await asyncio.sleep(1)
@ -659,6 +666,11 @@ class BluesoundPlayer(MediaPlayerDevice):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property
def bluesound_device_name(self):
"""Return the device name as returned by the device."""
return self._bluesound_device_name
@property @property
def icon(self): def icon(self):
"""Return the icon of the device.""" """Return the icon of the device."""
@ -690,7 +702,6 @@ class BluesoundPlayer(MediaPlayerDevice):
@property @property
def source(self): def source(self):
"""Name of the current input source.""" """Name of the current input source."""
if self._status is None or (self.is_grouped and not self.is_master): if self._status is None or (self.is_grouped and not self.is_master):
return None return None
@ -831,6 +842,39 @@ class BluesoundPlayer(MediaPlayerDevice):
else: else:
_LOGGER.error("Master not found %s", master_device) _LOGGER.error("Master not found %s", master_device)
@property
def device_state_attributes(self):
"""List members in group."""
attributes = {}
if self._group_list:
attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
attributes[ATTR_MASTER] = self._is_master
return attributes
def rebuild_bluesound_group(self):
"""Rebuild the list of entities in speaker group."""
if self._group_name is None:
return None
bluesound_group = []
device_group = self._group_name.split("+")
sorted_entities = sorted(
self._hass.data[DATA_BLUESOUND],
key=lambda entity: entity.is_master,
reverse=True,
)
bluesound_group = [
entity.name
for entity in sorted_entities
if entity.bluesound_device_name in device_group
]
return bluesound_group
async def async_unjoin(self): async def async_unjoin(self):
"""Unjoin the player from a group.""" """Unjoin the player from a group."""
if self._master is None: if self._master is None:

View File

@ -3,7 +3,7 @@
"name": "BMW Connected Drive", "name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": [ "requirements": [
"bimmer_connected==0.6.0" "bimmer_connected==0.6.2"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -4,10 +4,12 @@
"host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430"
}, },
"error": { "error": {
"certificate_error": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d",
"certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442",
"connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441",
"host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430",
"resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d",
"wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u0441\u044a\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0430 \u043d\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -0,0 +1,8 @@
{
"config": {
"error": {
"certificate_error": "Certifik\u00e1t nelze ov\u011b\u0159it",
"wrong_host": "Certifik\u00e1t neodpov\u00edd\u00e1 n\u00e1zvu hostitele"
}
}
}

View File

@ -4,10 +4,12 @@
"host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada"
}, },
"error": { "error": {
"certificate_error": "El certificado no pudo ser validado",
"certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto",
"connection_timeout": "Tiempo de espera agotado al conectar a este host", "connection_timeout": "Tiempo de espera agotado al conectar a este host",
"host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada",
"resolve_failed": "Este host no se puede resolver" "resolve_failed": "Este host no se puede resolver",
"wrong_host": "El certificado no coincide con el nombre de host"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata" "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata"
}, },
"error": { "error": {
"certificate_error": "Il certificato non pu\u00f2 essere convalidato",
"certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta",
"connection_timeout": "Tempo scaduto collegandosi a questo host", "connection_timeout": "Tempo scaduto collegandosi a questo host",
"host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata",
"resolve_failed": "Questo host non pu\u00f2 essere risolto" "resolve_failed": "Questo host non pu\u00f2 essere risolto",
"wrong_host": "Il certificato non corrisponde al nome host"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
}, },
"error": { "error": {
"certificate_error": "\uc778\uc99d\uc11c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4",
"host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"wrong_host": "\uc778\uc99d\uc11c\uac00 \ud638\uc2a4\ud2b8 \uc774\ub984\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert"
}, },
"error": { "error": {
"certificate_error": "Zertifikat konnt net valid\u00e9iert ginn",
"certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren",
"connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.",
"host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert",
"resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn",
"wrong_host": "Zertifikat entspr\u00e9cht net den Numm vum Apparat"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana"
}, },
"error": { "error": {
"certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu",
"certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu",
"connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.",
"host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana",
"resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107" "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107",
"wrong_host": "Certyfikat nie pasuje do nazwy hosta"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana" "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana"
}, },
"error": { "error": {
"certificate_error": "Certifikata ni bilo mogo\u010de preveriti",
"certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila",
"connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla",
"host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana",
"resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti",
"wrong_host": "Potrdilo se ne ujema z imenom gostitelja"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
}, },
"error": { "error": {
"certificate_error": "\u8a8d\u8b49\u7121\u6cd5\u78ba\u8a8d",
"certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49", "certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49",
"connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642", "connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642",
"host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790" "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790",
"wrong_host": "\u8a8d\u8b49\u8207\u4e3b\u6a5f\u540d\u7a31\u4e0d\u7b26\u5408"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -0,0 +1,16 @@
{
"device_automation": {
"action_type": {
"set_hvac_mode": "Canvia el mode HVAC de {entity_name}"
},
"condtion_type": {
"is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic",
"is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic"
},
"trigger_type": {
"current_humidity_changed": "Ha canviat la humitat mesurada per {entity_name}",
"current_temperature_changed": "Ha canviat la temperatura mesurada per {entity_name}",
"hvac_mode_changed": "El mode HVAC de {entity_name} ha canviat"
}
}
}

View File

@ -0,0 +1,17 @@
{
"device_automation": {
"action_type": {
"set_hvac_mode": "Change HVAC mode on {entity_name}",
"set_preset_mode": "Change preset on {entity_name}"
},
"condtion_type": {
"is_hvac_mode": "{entity_name} is set to a specific HVAC mode",
"is_preset_mode": "{entity_name} is set to a specific preset mode"
},
"trigger_type": {
"current_humidity_changed": "{entity_name} measured humidity changed",
"current_temperature_changed": "{entity_name} measured temperature changed",
"hvac_mode_changed": "{entity_name} HVAC mode changed"
}
}
}

View File

@ -0,0 +1,17 @@
{
"device_automation": {
"action_type": {
"set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.",
"set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}"
},
"condtion_type": {
"is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico",
"is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico"
},
"trigger_type": {
"current_humidity_changed": "{entity_name} humedad medida cambi\u00f3",
"current_temperature_changed": "{entity_name} temperatura medida cambi\u00f3",
"hvac_mode_changed": "{entity_name} Modo HVAC cambiado"
}
}
}

View File

@ -0,0 +1,12 @@
{
"device_automation": {
"action_type": {
"set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}"
},
"trigger_type": {
"current_humidity_changed": "Changement d'humidit\u00e9 mesur\u00e9e pour {entity_name}",
"current_temperature_changed": "Changement de temp\u00e9rature mesur\u00e9e pour {entity_name}",
"hvac_mode_changed": "Mode HVAC chang\u00e9 pour {entity_name}"
}
}
}

View File

@ -0,0 +1,17 @@
{
"device_automation": {
"action_type": {
"set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}",
"set_preset_mode": "Modifica preimpostazione su {entity_name}"
},
"condtion_type": {
"is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica",
"is_preset_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 preimpostata specifica"
},
"trigger_type": {
"current_humidity_changed": "{entity_name} umidit\u00e0 misurata modificata",
"current_temperature_changed": "{entity_name} temperatura misurata cambiata",
"hvac_mode_changed": "{entity_name} modalit\u00e0 HVAC modificata"
}
}
}

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