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/avion/light.py
homeassistant/components/azure_event_hub/*
homeassistant/components/azure_service_bus/*
homeassistant/components/baidu/tts.py
homeassistant/components/beewi_smartclim/sensor.py
homeassistant/components/bbb_gpio/*
@ -274,7 +275,6 @@ omit =
homeassistant/components/growatt_server/sensor.py
homeassistant/components/gstreamer/media_player.py
homeassistant/components/gtfs/sensor.py
homeassistant/components/gtt/sensor.py
homeassistant/components/habitica/*
homeassistant/components/hangouts/*
homeassistant/components/hangouts/__init__.py
@ -499,6 +499,7 @@ omit =
homeassistant/components/panasonic_bluray/media_player.py
homeassistant/components/panasonic_viera/media_player.py
homeassistant/components/pandora/media_player.py
homeassistant/components/pcal9535a/*
homeassistant/components/pencom/switch.py
homeassistant/components/philips_js/media_player.py
homeassistant/components/pi_hole/sensor.py
@ -720,6 +721,7 @@ omit =
homeassistant/components/uber/sensor.py
homeassistant/components/ubus/device_tracker.py
homeassistant/components/ue_smart_radio/media_player.py
homeassistant/components/unifiled/*
homeassistant/components/upcloud/*
homeassistant/components/upnp/*
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:
- repo: https://github.com/psf/black
rev: 19.3b0
rev: 19.10b0
hooks:
- id: black
args:
@ -8,24 +15,10 @@ repos:
- --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.8
rev: 3.7.9
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.3.1
- pydocstyle==4.0.0
- 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

@ -19,7 +19,7 @@ matrix:
- python: "3.6.1"
env: TOXENV=lint
- 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"
env: TOXENV=typing
- python: "3.6.1"
@ -33,4 +33,4 @@ cache:
- $HOME/.cache/pre-commit
install: pip install -U tox
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",
"type": "shell",
"command": "flake8 homeassistant tests",
"command": "pre-commit run flake8 --all-files",
"group": {
"kind": "test",
"isDefault": true

View File

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

View File

@ -45,7 +45,7 @@ stages:
. venv/bin/activate
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: |
. venv/bin/activate
pre-commit run flake8 --all-files
@ -84,7 +84,7 @@ stages:
. venv/bin/activate
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: |
. venv/bin/activate
pre-commit run black --all-files
@ -127,7 +127,7 @@ stages:
set -e
. 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
displayName: 'Run pytest for python $(python.container)'
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
@ -135,7 +135,7 @@ stages:
set -e
. 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)
script/check_dirty
displayName: 'Run pytest for python $(python.container) / coverage'
@ -182,8 +182,8 @@ stages:
. venv/bin/activate
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: |
. venv/bin/activate
pre-commit run mypy --all-files
pre-commit run --config .pre-commit-config-all.yaml mypy --all-files
displayName: 'Run mypy'

View File

@ -261,7 +261,7 @@ class AuthManager:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
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)
@ -276,7 +276,7 @@ class AuthManager:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
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)
@ -320,7 +320,7 @@ class AuthManager:
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
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:
@ -330,7 +330,7 @@ class AuthManager:
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
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:
for token in user.refresh_tokens.values():

View File

@ -215,7 +215,11 @@ class TotpSetupFlow(SetupFlow):
else:
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
str(self._user.name),
)

View File

@ -33,6 +33,8 @@ STAGE_1_INTEGRATIONS = {
"recorder",
# To make sure we forward data to other instances
"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": {
"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": {
"user": {
"data": {
"password": "Senha",
"username": "Endere\u00e7o de e-mail"
}
}

View File

@ -20,10 +20,17 @@ from homeassistant.const import (
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers import config_validation as cv
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__)
@ -89,7 +96,7 @@ class AbodeSystem:
self.abode = abode
self.polling = polling
self.devices = []
self.entity_ids = set()
self.logout_listener = None
@ -179,27 +186,29 @@ def setup_hass_services(hass):
"""Capture a new image."""
entity_ids = call.data.get(ATTR_ENTITY_ID)
target_devices = [
device
for device in hass.data[DOMAIN].devices
if device.entity_id in entity_ids
target_entities = [
entity_id
for entity_id in hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for device in target_devices:
device.capture()
for entity_id in target_entities:
signal = SIGNAL_CAPTURE_IMAGE.format(entity_id)
dispatcher_send(hass, signal)
def trigger_quick_action(call):
"""Trigger a quick action."""
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
target_devices = [
device
for device in hass.data[DOMAIN].devices
if device.entity_id in entity_ids
target_entities = [
entity_id
for entity_id in hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for device in target_devices:
device.trigger()
for entity_id in target_entities:
signal = SIGNAL_TRIGGER_QUICK_ACTION.format(entity_id)
dispatcher_send(hass, signal)
hass.services.register(
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
@ -290,6 +299,7 @@ class AbodeDevice(Entity):
self._device.device_id,
self._update_callback,
)
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
async def async_will_remove_from_hass(self):
"""Unsubscribe from device events."""
@ -352,13 +362,14 @@ class AbodeAutomation(Entity):
self._event = event
async def async_added_to_hass(self):
"""Subscribe Abode events."""
"""Subscribe to a group of Abode timeline events."""
if self._event:
self.hass.async_add_job(
self._data.abode.events.add_event_callback,
self._event,
self._update_callback,
)
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
@property
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):
"""Set up an alarm control panel for an Abode device."""
"""Set up Abode alarm control panel device."""
data = hass.data[DOMAIN]
async_add_entities(
[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
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import AbodeAutomation, AbodeDevice
from .const import DOMAIN
from .const import DOMAIN, SIGNAL_TRIGGER_QUICK_ACTION
_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):
"""Set up a sensor for an Abode device."""
"""Set up Abode binary sensor devices."""
data = hass.data[DOMAIN]
device_types = [
@ -29,19 +30,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
CONST.TYPE_OPENING,
]
devices = []
entities = []
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):
devices.append(
entities.append(
AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
)
)
async_add_entities(devices)
async_add_entities(entities)
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
@ -61,6 +62,12 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice):
"""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):
"""Trigger a quick automation."""
self._automation.trigger()

View File

@ -7,10 +7,11 @@ import abodepy.helpers.timeline as TIMELINE
import requests
from homeassistant.components.camera import Camera
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import Throttle
from . import AbodeDevice
from .const import DOMAIN
from .const import DOMAIN, SIGNAL_CAPTURE_IMAGE
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):
"""Set up a camera for an Abode device."""
"""Set up Abode camera devices."""
data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA):
devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE))
entities = []
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):
@ -54,6 +55,9 @@ class AbodeCamera(AbodeDevice, Camera):
self._capture_callback,
)
signal = SIGNAL_CAPTURE_IMAGE.format(self.entity_id)
async_dispatcher_connect(self.hass, signal, self.capture)
def capture(self):
"""Request a new image capture."""
return self._device.capture()

View File

@ -3,3 +3,6 @@ DOMAIN = "abode"
ATTRIBUTION = "Data provided by goabode.com"
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):
"""Set up Abode cover devices."""
data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER):
devices.append(AbodeCover(data, device))
entities = []
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):

View File

@ -33,12 +33,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Abode light devices."""
data = hass.data[DOMAIN]
devices = []
entities = []
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):

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):
"""Set up Abode lock devices."""
data = hass.data[DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK):
devices.append(AbodeLock(data, device))
entities = []
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):

View File

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

View File

@ -16,9 +16,9 @@ _LOGGER = logging.getLogger(__name__)
# Sensor types: Name, icon
SENSOR_TYPES = {
"temp": ["Temperature", DEVICE_CLASS_TEMPERATURE],
"humidity": ["Humidity", DEVICE_CLASS_HUMIDITY],
"lux": ["Lux", DEVICE_CLASS_ILLUMINANCE],
CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE],
CONST.HUMI_STATUS_KEY: ["Humidity", DEVICE_CLASS_HUMIDITY],
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):
"""Set up a sensor for an Abode device."""
"""Set up Abode sensor devices."""
data = hass.data[DOMAIN]
devices = []
entities = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR):
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):
@ -62,6 +64,11 @@ class AbodeSensor(AbodeDevice):
"""Return the 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
def state(self):
"""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."""
data = hass.data[DOMAIN]
devices = []
entities = []
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):
devices.append(
entities.append(
AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)
)
async_add_entities(devices)
async_add_entities(entities)
class AbodeSwitch(AbodeDevice, SwitchDevice):

View File

@ -1,6 +1,8 @@
{
"config": {
"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.",
"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": {
"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.",
"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": {
"action_type": {
"arm_away": "Inschakelen {entity_name} afwezig",
"arm_home": "Inschakelen {entity_name} thuis",
"arm_night": "Inschakelen {entity_name} nacht",
"disarm": "Uitschakelen {entity_name}",
"trigger": "Trigger {entity_name}"
}

View File

@ -1,6 +1,10 @@
{
"device_automation": {
"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}"
}
}

View File

@ -1,6 +1,10 @@
{
"device_automation": {
"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"
}
}

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.
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:
description: Send the alarm the command for arm home.
fields:

View File

@ -12,11 +12,14 @@ from homeassistant.const import (
STATE_LOCKED,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_UNAVAILABLE,
STATE_UNLOCKED,
STATE_UNKNOWN,
STATE_UNLOCKED,
)
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 import light, fan, cover
import homeassistant.util.color as color_util
@ -110,6 +113,11 @@ class AlexaCapability:
"""Return the Configuration object."""
return []
@staticmethod
def supported_operations():
"""Return the supportedOperations object."""
return []
def serialize_discovery(self):
"""Serialize according to the Discovery API."""
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
@ -150,6 +158,10 @@ class AlexaCapability:
if instance is not None:
result["instance"] = instance
supported_operations = self.supported_operations()
if supported_operations:
result["supportedOperations"] = supported_operations
return result
def serialize_properties(self):
@ -484,6 +496,28 @@ class AlexaPlaybackController(AlexaCapability):
"""Return the Alexa API name of this interface."""
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):
"""Implements Alexa.InputController.
@ -704,6 +738,33 @@ class AlexaThermostatController(AlexaCapability):
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):
"""Implements Alexa.PowerLevelController.
@ -1078,3 +1139,50 @@ class AlexaDoorbellEventSource(AlexaCapability):
def capability_proactively_reported(self):
"""Return True for proactively reported capability."""
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_OFF, "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"}
PERCENTAGE_FAN_MAP = {

View File

@ -46,11 +46,13 @@ from .capabilities import (
AlexaMotionSensor,
AlexaPercentageController,
AlexaPlaybackController,
AlexaPlaybackStateReporter,
AlexaPowerController,
AlexaPowerLevelController,
AlexaRangeController,
AlexaSceneController,
AlexaSecurityPanelController,
AlexaSeekController,
AlexaSpeaker,
AlexaStepSpeaker,
AlexaTemperatureSensor,
@ -391,6 +393,10 @@ class MediaPlayerCapabilities(AlexaEntity):
def default_display_categories(self):
"""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]
def interfaces(self):
@ -418,6 +424,10 @@ class MediaPlayerCapabilities(AlexaEntity):
)
if supported & playback_features:
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:
yield AlexaInputController(self.entity)

View File

@ -111,3 +111,10 @@ class AlexaInvalidDirectiveError(AlexaError):
namespace = "Alexa"
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 (
API_TEMP_UNITS,
API_THERMOSTAT_MODES_CUSTOM,
API_THERMOSTAT_MODES,
API_THERMOSTAT_PRESETS,
Cause,
@ -53,6 +54,7 @@ from .errors import (
AlexaSecurityPanelUnauthorizedError,
AlexaTempRangeError,
AlexaUnsupportedThermostatModeError,
AlexaVideoActionNotPermittedForContentError,
)
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)
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:
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:
msg = f"The requested thermostat mode {mode} is not supported"
raise AlexaUnsupportedThermostatModeError(msg)
@ -1168,3 +1187,45 @@ async def async_api_skipchannel(hass, config, directive, context):
)
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",
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"requirements": [
"boto3==1.9.233"
"boto3==1.9.252"
],
"dependencies": [],
"codeowners": [
"@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."""
output_format = config.get(CONF_OUTPUT_FORMAT)
sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])

View File

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

View File

@ -287,8 +287,11 @@ class ADBDevice(MediaPlayerDevice):
"""Initialize the Android TV / Fire TV device."""
self.aftv = aftv
self._name = name
self._apps = APPS.copy()
self._apps.update(apps)
self._app_id_to_name = APPS.copy()
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._device_properties = self.aftv.device_properties
@ -328,7 +331,7 @@ class ADBDevice(MediaPlayerDevice):
@property
def app_name(self):
"""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
def available(self):
@ -455,9 +458,13 @@ class AndroidTVDevice(ADBDevice):
return
# 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)
if self._state is None:
@ -514,7 +521,7 @@ class FireTVDevice(ADBDevice):
super().__init__(aftv, name, apps, turn_on_command, turn_off_command)
self._get_sources = get_sources
self._running_apps = None
self._sources = None
@adb_decorator(override_available=True)
def update(self):
@ -534,23 +541,28 @@ class FireTVDevice(ADBDevice):
return
# Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._running_apps = self.aftv.update(
self._get_sources
)
state, self._current_app, running_apps = self.aftv.update(self._get_sources)
self._state = ANDROIDTV_STATES.get(state)
if self._state is None:
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
def source(self):
"""Return the current app."""
return self._current_app
return self._app_id_to_name.get(self._current_app, self._current_app)
@property
def source_list(self):
"""Return a list of running apps."""
return self._running_apps
return self._sources
@property
def supported_features(self):
@ -571,6 +583,7 @@ class FireTVDevice(ADBDevice):
"""
if isinstance(source, str):
if not source.startswith("!"):
self.aftv.launch_app(source)
self.aftv.launch_app(self._app_name_to_id.get(source, source))
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",
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
"requirements": [
"aioasuswrt==1.1.21"
"aioasuswrt==1.1.22"
],
"dependencies": [],
"codeowners": [

View File

@ -25,7 +25,7 @@
},
"step": {
"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"
}
},

View File

@ -4,7 +4,7 @@ import logging
import voluptuous as vol
from homeassistant import exceptions
from homeassistant.core import callback
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.const import (
CONF_VALUE_TEMPLATE,
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
# 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(
vol.Schema(
@ -42,7 +43,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="numeric_state"
):
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
@ -52,7 +53,7 @@ async def async_attach_trigger(
value_template = config.get(CONF_VALUE_TEMPLATE)
unsub_track_same = {}
entities_triggered = set()
period = {}
period: dict = {}
if value_template is not None:
value_template.hass = hass

View File

@ -27,8 +27,8 @@ TRIGGER_SCHEMA = vol.All(
vol.Required(CONF_PLATFORM): "state",
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions
vol.Optional(CONF_FROM): str,
vol.Optional(CONF_TO): str,
vol.Optional(CONF_FROM): vol.Any(str, [str]),
vol.Optional(CONF_TO): vol.Any(str, [str]),
vol.Optional(CONF_FOR): vol.Any(
vol.All(cv.time_period, cv.positive_timedelta),
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."""
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass
@ -65,7 +67,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
variables = {
"trigger": {
"platform": "template",
"platform": platform_type,
"entity_id": entity_id,
"from_state": from_s,
"to_state": to_s,

View File

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

View File

@ -3,7 +3,7 @@
"name": "Aws",
"documentation": "https://www.home-assistant.io/integrations/aws",
"requirements": [
"aiobotocore==0.10.2"
"aiobotocore==0.10.4"
],
"dependencies": [],
"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",
"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": {
"user": {
"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",
"faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas"
},
"flow_title": "Eixos do dispositivo: {name} ({host})",
"step": {
"user": {
"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]
def get_engine(hass, config):
def get_engine(hass, config, discovery_info=None):
"""Set up Baidu TTS component."""
return BaiduTTSProvider(hass, config)

View File

@ -53,6 +53,7 @@
"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",
"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",
"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",
@ -71,6 +72,7 @@
"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_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_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",

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",
"sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son",
"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",
"vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations"
}

View File

@ -46,13 +46,14 @@
},
"trigger_type": {
"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",
"connected": "{entity_name} csatlakozott",
"connected": "{entity_name} csatlakozik",
"gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel",
"hot": "{entity_name} felforr\u00f3sodott",
"hot": "{entity_name} felforr\u00f3sodik",
"light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel",
"locked": "{entity_name} be lett z\u00e1rva",
"moist": "{entity_name} nedves lett",
"moist\u00a7": "{entity_name} nedves lett",
"motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel",
"moving": "{entity_name} mozog",
@ -65,12 +66,13 @@
"no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st",
"not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151",
"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_locked": "{entity_name} ki lett nyitva",
"not_moist": "{entity_name} sz\u00e1raz lett",
"not_moving": "{entity_name} m\u00e1r nem mozog",
"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_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt",
"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_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_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_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",
@ -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_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_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_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",

View File

@ -1,6 +1,6 @@
"""Support for Bluesound devices."""
import asyncio
from asyncio.futures import CancelledError
from asyncio import CancelledError
from datetime import timedelta
import logging
from urllib import parse
@ -53,6 +53,7 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
ATTR_BLUESOUND_GROUP = "bluesound_group"
ATTR_MASTER = "master"
DATA_BLUESOUND = "bluesound"
@ -219,6 +220,8 @@ class BluesoundPlayer(MediaPlayerDevice):
self._master = None
self._is_master = False
self._group_name = None
self._group_list = []
self._bluesound_device_name = None
self._init_callback = init_callback
if self.port is None:
@ -247,6 +250,8 @@ class BluesoundPlayer(MediaPlayerDevice):
if not self._name:
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:
self._icon = self._sync_status.get("@icon", self.host)
@ -331,7 +336,6 @@ class BluesoundPlayer(MediaPlayerDevice):
self, method, raise_timeout=False, allow_offline=False
):
"""Send command to the player."""
if not self._is_online and not allow_offline:
return
@ -371,7 +375,6 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_update_status(self):
"""Use the poll session to always get the status of the player."""
response = None
url = "Status"
@ -402,6 +405,10 @@ class BluesoundPlayer(MediaPlayerDevice):
if group_name != self._group_name:
_LOGGER.debug("Group name change detected on device: %s", self.host)
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
# devices is synced
await asyncio.sleep(1)
@ -659,6 +666,11 @@ class BluesoundPlayer(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
@property
def bluesound_device_name(self):
"""Return the device name as returned by the device."""
return self._bluesound_device_name
@property
def icon(self):
"""Return the icon of the device."""
@ -690,7 +702,6 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def source(self):
"""Name of the current input source."""
if self._status is None or (self.is_grouped and not self.is_master):
return None
@ -831,6 +842,39 @@ class BluesoundPlayer(MediaPlayerDevice):
else:
_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):
"""Unjoin the player from a group."""
if self._master is None:

View File

@ -3,7 +3,7 @@
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": [
"bimmer_connected==0.6.0"
"bimmer_connected==0.6.2"
],
"dependencies": [],
"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"
},
"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",
"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",
"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": {
"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"
},
"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",
"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",
"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": {
"user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata"
},
"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",
"connection_timeout": "Tempo scaduto collegandosi a questo host",
"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": {
"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"
},
"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",
"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",
"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": {
"user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert"
},
"error": {
"certificate_error": "Zertifikat konnt net valid\u00e9iert ginn",
"certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren",
"connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.",
"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": {
"user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana"
},
"error": {
"certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu",
"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",
"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": {
"user": {

View File

@ -4,10 +4,12 @@
"host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana"
},
"error": {
"certificate_error": "Certifikata ni bilo mogo\u010de preveriti",
"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",
"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": {
"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"
},
"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",
"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",
"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": {
"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